diff --git a/pkg/detectors/jiratoken/jiratoken.go b/pkg/detectors/jiratoken/jiratoken.go index 4b5ed368e618..0384afed928f 100644 --- a/pkg/detectors/jiratoken/jiratoken.go +++ b/pkg/detectors/jiratoken/jiratoken.go @@ -13,16 +13,18 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" ) -type Scanner struct{} +type Scanner struct { + client *http.Client +} // Ensure the Scanner satisfies the interface at compile time. var _ detectors.Detector = (*Scanner)(nil) var _ detectors.Versioner = (*Scanner)(nil) -func (Scanner) Version() int { return 1 } +func (Scanner) Version() int { return 1 } var ( - client = common.SaneHttpClient() + defaultClient = common.SaneHttpClient() // Make sure that your group is surrounded in boundary characters such as below to reduce false positives. tokenPat = regexp.MustCompile(detectors.PrefixRegex([]string{"jira"}) + `\b([a-zA-Z-0-9]{24})\b`) @@ -73,6 +75,11 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { + client := s.client + if client == nil { + client = defaultClient + } + data := fmt.Sprintf("%s:%s", resEmail, resToken) sEnc := b64.StdEncoding.EncodeToString([]byte(data)) req, err := http.NewRequestWithContext(ctx, "GET", "https://"+resDomain+"/rest/api/3/dashboard", nil) @@ -88,9 +95,15 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result // If the request is successful and the login reason is not failed authentication, then the token is valid. // This is because Jira returns a 200 status code even if the token is invalid. // Jira returns a default dashboard page. - if (res.StatusCode >= 200 && res.StatusCode < 300) && res.Header.Get(loginReasonHeaderKey) != failedAuth { - s1.Verified = true + if res.StatusCode >= 200 && res.StatusCode < 300 { + if res.Header.Get(loginReasonHeaderKey) != failedAuth { + s1.Verified = true + } + } else { + s1.VerificationError = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) } + } else { + s1.VerificationError = err } } diff --git a/pkg/detectors/jiratoken/jiratoken_test.go b/pkg/detectors/jiratoken/jiratoken_test.go index 460992e5dc25..d733b0b92481 100644 --- a/pkg/detectors/jiratoken/jiratoken_test.go +++ b/pkg/detectors/jiratoken/jiratoken_test.go @@ -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/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/common" @@ -23,8 +24,8 @@ func TestJiraToken_FromChunk(t *testing.T) { if err != nil { t.Fatalf("could not get test secrets from GCP: %s", err) } - token := testSecrets.MustGetField("JIRA_TOKEN") - inactiveToken := testSecrets.MustGetField("JIRA_INACTIVE") + secret := testSecrets.MustGetField("JIRA_TOKEN") + inactiveSecret := testSecrets.MustGetField("JIRA_INACTIVE") email := testSecrets.MustGetField("JIRA_EMAIL") domain := testSecrets.MustGetField("JIRA_DOMAIN") @@ -34,18 +35,19 @@ func TestJiraToken_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", s: Scanner{}, args: args{ ctx: context.Background(), - data: []byte(fmt.Sprintf("You can find a jira secret %s within jira %s with jira %s", token, email, domain)), + data: []byte(fmt.Sprintf("You can find a jira secret %s within jira %s with jira %s", secret, email, domain)), verify: true, }, want: []detectors.Result{ @@ -54,14 +56,15 @@ func TestJiraToken_FromChunk(t *testing.T) { Verified: true, }, }, - wantErr: false, + wantErr: false, + wantVerificationErr: false, }, { name: "found, unverified", s: Scanner{}, args: args{ ctx: context.Background(), - data: []byte(fmt.Sprintf("You can find a jira secret %s within jira %s but not jira %s valid", inactiveToken, email, domain)), // the secret would satisfy the regex but not pass validation + data: []byte(fmt.Sprintf("You can find a jira secret %s within jira %s but not jira %s valid", inactiveSecret, email, domain)), // the secret would satisfy the regex but not pass validation verify: true, }, want: []detectors.Result{ @@ -70,7 +73,8 @@ func TestJiraToken_FromChunk(t *testing.T) { Verified: false, }, }, - wantErr: false, + wantErr: false, + wantVerificationErr: false, }, { name: "not found", @@ -80,14 +84,48 @@ func TestJiraToken_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 jira secret %s within jira %s with jira %s", secret, email, domain)), + verify: true, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_JiraToken, + 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 jira secret %s within jira %s with jira %s", secret, email, domain)), + verify: true, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_JiraToken, + 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("JiraToken.FromData() error = %v, wantErr %v", err, tt.wantErr) return @@ -96,10 +134,12 @@ func TestJiraToken_FromChunk(t *testing.T) { if len(got[i].Raw) == 0 { t.Fatalf("no raw secret present: \n %+v", got[i]) } - got[i].Raw = nil - got[i].RawV2 = 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.want); diff != "" { + ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "VerificationError") + if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" { t.Errorf("JiraToken.FromData() %s diff: (-got +want)\n%s", tt.name, diff) } }) diff --git a/pkg/detectors/jiratoken_v2/jiratoken_v2.go b/pkg/detectors/jiratoken_v2/jiratoken_v2.go index f6c18e01714a..26959e857936 100644 --- a/pkg/detectors/jiratoken_v2/jiratoken_v2.go +++ b/pkg/detectors/jiratoken_v2/jiratoken_v2.go @@ -13,16 +13,18 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" ) -type Scanner struct{} +type Scanner struct { + client *http.Client +} // Ensure the Scanner satisfies the interface at compile time. var _ detectors.Detector = (*Scanner)(nil) var _ detectors.Versioner = (*Scanner)(nil) -func (Scanner) Version() int { return 2 } +func (Scanner) Version() int { return 2 } var ( - client = common.SaneHttpClient() + defaultClient = common.SaneHttpClient() // https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/ // Tokens created after Jan 18 2023 use a variable length @@ -74,6 +76,11 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { + client := s.client + if client == nil { + client = defaultClient + } + data := fmt.Sprintf("%s:%s", resEmail, resToken) sEnc := b64.StdEncoding.EncodeToString([]byte(data)) req, err := http.NewRequestWithContext(ctx, "GET", "https://"+resDomain+"/rest/api/3/dashboard", nil) @@ -89,11 +96,15 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result // If the request is successful and the login reason is not failed authentication, then the token is valid. // This is because Jira returns a 200 status code even if the token is invalid. // Jira returns a default dashboard page. - if (res.StatusCode >= 200 && res.StatusCode < 300) && res.Header.Get(loginReasonHeaderKey) != failedAuth { - s1.Verified = true + if res.StatusCode >= 200 && res.StatusCode < 300 { + if res.Header.Get(loginReasonHeaderKey) != failedAuth { + s1.Verified = true + } } else { - s1.VerificationError = err + s1.VerificationError = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) } + } else { + s1.VerificationError = err } } diff --git a/pkg/detectors/jiratoken_v2/jiratoken_v2_test.go b/pkg/detectors/jiratoken_v2/jiratoken_v2_test.go index cdcc6e875dc1..bdfd2454ce20 100644 --- a/pkg/detectors/jiratoken_v2/jiratoken_v2_test.go +++ b/pkg/detectors/jiratoken_v2/jiratoken_v2_test.go @@ -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/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/common" @@ -23,8 +24,8 @@ func TestJiraToken_FromChunk(t *testing.T) { if err != nil { t.Fatalf("could not get test secrets from GCP: %s", err) } - token := testSecrets.MustGetField("JIRA_V2_TOKEN") - inactiveToken := testSecrets.MustGetField("JIRA_V2_INACTIVE") + secret := testSecrets.MustGetField("JIRA_V2_TOKEN") + inactiveSecret := testSecrets.MustGetField("JIRA_V2_INACTIVE") email := testSecrets.MustGetField("JIRA_V2_EMAIL") domain := testSecrets.MustGetField("JIRA_V2_DOMAIN") @@ -34,18 +35,19 @@ func TestJiraToken_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", s: Scanner{}, args: args{ ctx: context.Background(), - data: []byte(fmt.Sprintf("You can find a jira secret %s within jira %s with jira %s", token, email, domain)), + data: []byte(fmt.Sprintf("You can find a jira secret %s within jira %s with jira %s", secret, email, domain)), verify: true, }, want: []detectors.Result{ @@ -54,14 +56,15 @@ func TestJiraToken_FromChunk(t *testing.T) { Verified: true, }, }, - wantErr: false, + wantErr: false, + wantVerificationErr: false, }, { name: "found, unverified", s: Scanner{}, args: args{ ctx: context.Background(), - data: []byte(fmt.Sprintf("You can find a jira secret %s within jira %s but not jira %s valid", inactiveToken, email, domain)), // the secret would satisfy the regex but not pass validation + data: []byte(fmt.Sprintf("You can find a jira secret %s within jira %s but not jira %s valid", inactiveSecret, email, domain)), // the secret would satisfy the regex but not pass validation verify: true, }, want: []detectors.Result{ @@ -70,7 +73,8 @@ func TestJiraToken_FromChunk(t *testing.T) { Verified: false, }, }, - wantErr: false, + wantErr: false, + wantVerificationErr: false, }, { name: "not found", @@ -80,14 +84,48 @@ func TestJiraToken_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 jira secret %s within jira %s with jira %s", secret, email, domain)), + verify: true, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_JiraToken, + 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 jira secret %s within jira %s with jira %s", secret, email, domain)), + verify: true, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_JiraToken, + 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("JiraToken.FromData() error = %v, wantErr %v", err, tt.wantErr) return @@ -96,10 +134,12 @@ func TestJiraToken_FromChunk(t *testing.T) { if len(got[i].Raw) == 0 { t.Fatalf("no raw secret present: \n %+v", got[i]) } - got[i].Raw = nil - got[i].RawV2 = 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.want); diff != "" { + ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "VerificationError") + if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" { t.Errorf("JiraToken.FromData() %s diff: (-got +want)\n%s", tt.name, diff) } })