@@ -4,99 +4,128 @@ import (
4
4
"context"
5
5
"encoding/base64"
6
6
"fmt"
7
+ "io"
7
8
"net/http"
8
9
"regexp"
9
- "strings"
10
10
11
11
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
12
12
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
13
13
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
14
14
)
15
15
16
- type Scanner struct {}
16
+ // Scanner is a stateless struct that implements the detector interface.
17
+ type Scanner struct {
18
+ client * http.Client
19
+ }
17
20
18
21
// Ensure the Scanner satisfies the interface at compile time.
19
22
var _ detectors.Detector = (* Scanner )(nil )
20
23
21
- var (
22
- client = common .SaneHttpClient ()
24
+ // Keywords are used for efficiently pre-filtering chunks.
25
+ func (s Scanner ) Keywords () []string {
26
+ return []string {"bitbucket" , "ATBB" }
27
+ }
23
28
24
- // Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
25
- // The following patterns cover the methods of authentication found here:
26
- // https://support.atlassian.com/bitbucket-cloud/docs/using-app-passwords/, as well as for other general cases.
29
+ func ( s Scanner ) Type () detectorspb. DetectorType {
30
+ return detectorspb . DetectorType_BitbucketAppPassword
31
+ }
27
32
28
- // Covers 'username:appPassword' pattern
29
- credentialPairPattern = regexp .MustCompile (`\b([A-Za-z0-9-_]{1,30}):ATBB[A-Za-z0-9_=.-]+[A-Z0-9]{8}\b` )
30
- // Covers assignment of username to variable
31
- usernameAssignmentPattern = regexp .MustCompile (`(?im)(?:user|usr)\S{0,40}?[:=\s]{1,3}[ '"=]?([a-zA-Z0-9-_]{1,30})\b` )
32
- // Covers 'https://[email protected] ' pattern
33
- usernameUrlPattern = regexp .MustCompile (`https://([a-zA-Z0-9-_]{1,30})@bitbucket.org` )
34
- // Covers '("username", "password")' pattern, used for HTTP Basic Auth
35
- httpBasicAuthPattern = regexp .MustCompile (`"([a-zA-Z0-9-_]{1,30})",(?: )?"ATBB[A-Za-z0-9_=.-]+[A-Z0-9]{8}"` )
33
+ func (s Scanner ) Description () string {
34
+ return "Bitbucket is a Git repository hosting service by Atlassian. Bitbucket App Passwords are used to authenticate to the Bitbucket API."
35
+ }
36
36
37
- usernamePatterns = [] * regexp. Regexp { usernamePat1 , usernamePat2 , usernamePat3 , usernamePat4 }
37
+ const bitbucketAPIUserURL = "https://api.bitbucket.org/2.0/user"
38
38
39
- appPasswordPat = regexp .MustCompile (`\bATBB[A-Za-z0-9_=.-]+[A-Z0-9]{8}\b` )
39
+ var (
40
+ defaultClient = common .SaneHttpClient ()
40
41
)
41
42
42
- // Keywords are used for efficiently pre-filtering chunks.
43
- // Use identifiers in the secret preferably, or the provider name.
44
- func (s Scanner ) Keywords () []string {
45
- return []string {"bitbucketapppassword" , "ATBB" }
46
- }
43
+ var (
44
+ // credentialPatterns uses named capture groups (?P<name>...) for readability and robustness.
45
+ credentialPatterns = []* regexp.Regexp {
46
+ // Explicitly define the boundary as (start of string) or (a non-username character).
47
+ regexp .MustCompile (`(?:^|[^A-Za-z0-9-_])(?P<username>[A-Za-z0-9-_]{1,30}):(?P<password>ATBB[A-Za-z0-9_=.-]+)\b` ),
48
+ // Catches 'https://username:[email protected] ' pattern
49
+ regexp .MustCompile (`https://(?P<username>[A-Za-z0-9-_]{1,30}):(?P<password>ATBB[A-Za-z0-9_=.-]+)@bitbucket\.org` ),
50
+ // Catches '("username", "password")' pattern, used for HTTP Basic Auth
51
+ regexp .MustCompile (`"(?P<username>[A-Za-z0-9-_]{1,30})",\s*"(?P<password>ATBB[A-Za-z0-9_=.-]+)"` ),
52
+ }
53
+ )
47
54
48
55
// FromData will find and optionally verify Bitbucket App Password secrets in a given set of bytes.
49
- func (s Scanner ) FromData (ctx context.Context , verify bool , data []byte ) (results []detectors.Result , err error ) {
56
+ func (s Scanner ) FromData (ctx context.Context , verify bool , data []byte ) ([]detectors.Result , error ) {
50
57
dataStr := string (data )
51
58
52
- var usernameMatches [][]string
53
- for _ , pattern := range usernamePatterns {
54
- usernameMatches = append (usernameMatches , pattern .FindAllStringSubmatch (dataStr , - 1 )... )
55
- }
56
- appPasswordMatches := appPasswordPat .FindAllString (dataStr , - 1 )
59
+ uniqueCredentials := make (map [string ]string )
57
60
58
- for _ , usernameMatch := range usernameMatches {
59
- if len (usernameMatch ) != 2 {
60
- continue
61
- }
62
- resUsernameMatch := strings .TrimSpace (usernameMatch [1 ])
61
+ for _ , pattern := range credentialPatterns {
62
+ for _ , match := range pattern .FindAllStringSubmatch (dataStr , - 1 ) {
63
+ // Extract credentials using named capture groups for readability.
64
+ namedMatches := make (map [string ]string )
65
+ for i , name := range pattern .SubexpNames () {
66
+ if i != 0 && name != "" {
67
+ namedMatches [name ] = match [i ]
68
+ }
69
+ }
63
70
64
- for _ , resAppPasswordMatch := range appPasswordMatches {
71
+ username := namedMatches ["username" ]
72
+ password := namedMatches ["password" ]
65
73
66
- s1 := detectors.Result {
67
- DetectorType : detectorspb .DetectorType_BitbucketAppPassword ,
68
- Raw : []byte (fmt .Sprintf (`%s: %s` , resUsernameMatch , resAppPasswordMatch )),
74
+ if username != "" && password != "" {
75
+ uniqueCredentials [username ] = password
69
76
}
77
+ }
78
+ }
70
79
71
- if verify {
72
- req , err := http .NewRequestWithContext (ctx , http .MethodGet , "https://api.bitbucket.org/2.0/user" , nil )
73
- if err != nil {
74
- continue
75
- }
76
- req .Header .Add ("Accept" , "application/json" )
77
- data := fmt .Sprintf ("%s:%s" , resUsernameMatch , resAppPasswordMatch )
78
- req .Header .Add ("Authorization" , fmt .Sprintf ("Basic %s" , base64 .StdEncoding .EncodeToString ([]byte (data ))))
79
- res , err := client .Do (req )
80
- if err == nil {
81
- defer res .Body .Close ()
82
- // Status 403 FORBIDDEN indicates a valid secret without valid scope
83
- if res .StatusCode >= 200 && res .StatusCode < 300 || res .StatusCode == 403 {
84
- s1 .Verified = true
85
- }
86
- }
80
+ var results []detectors.Result
81
+ for username , password := range uniqueCredentials {
82
+ result := detectors.Result {
83
+ DetectorType : detectorspb .DetectorType_BitbucketAppPassword ,
84
+ Raw : fmt .Appendf (nil , "%s:%s" , username , password ),
85
+ }
86
+ if verify {
87
+ client := s .client
88
+ if client == nil {
89
+ client = defaultClient
90
+ }
91
+ var vErr error
92
+ result .Verified , vErr = verifyCredential (ctx , client , username , password )
93
+ if vErr != nil {
94
+ result .SetVerificationError (vErr , username , password )
87
95
}
88
-
89
- results = append (results , s1 )
90
96
}
97
+ results = append (results , result )
91
98
}
92
99
93
100
return results , nil
94
101
}
95
102
96
- func (s Scanner ) Type () detectorspb.DetectorType {
97
- return detectorspb .DetectorType_BitbucketAppPassword
98
- }
103
+ // verifyCredential checks if a given username and app password are valid by making a request to the Bitbucket API.
104
+ func verifyCredential (ctx context.Context , client * http.Client , username , password string ) (bool , error ) {
105
+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , bitbucketAPIUserURL , nil )
106
+ if err != nil {
107
+ return false , err
108
+ }
109
+ req .Header .Add ("Accept" , "application/json" )
110
+ auth := base64 .StdEncoding .EncodeToString ([]byte (fmt .Sprintf ("%s:%s" , username , password )))
111
+ req .Header .Add ("Authorization" , fmt .Sprintf ("Basic %s" , auth ))
99
112
100
- func (s Scanner ) Description () string {
101
- return "Bitbucket is a Git repository hosting service by Atlassian. Bitbucket App Passwords are used to authenticate to the Bitbucket API."
113
+ res , err := client .Do (req )
114
+ if err != nil {
115
+ return false , err
116
+ }
117
+ defer func () {
118
+ _ , _ = io .Copy (io .Discard , res .Body )
119
+ _ = res .Body .Close ()
120
+ }()
121
+
122
+ switch res .StatusCode {
123
+ case http .StatusOK , http .StatusForbidden :
124
+ // A 403 can indicate a valid credential with insufficient scope, which is still a finding.
125
+ return true , nil
126
+ case http .StatusUnauthorized :
127
+ return false , nil
128
+ default :
129
+ return false , fmt .Errorf ("unexpected status code: %d" , res .StatusCode )
130
+ }
102
131
}
0 commit comments