Skip to content

Commit 626887c

Browse files
Improved and fixed couchbase detector (#4383)
1 parent 0ebf8ca commit 626887c

File tree

3 files changed

+111
-121
lines changed

3 files changed

+111
-121
lines changed

pkg/detectors/couchbase/couchbase.go

Lines changed: 82 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package couchbase
33
import (
44
"context"
55
"fmt"
6-
"strings"
76
"time"
87
"unicode"
98

@@ -23,35 +22,18 @@ type Scanner struct {
2322
var _ detectors.Detector = (*Scanner)(nil)
2423

2524
var (
26-
2725
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
28-
connectionStringPat = regexp.MustCompile(`\bcb\.[a-z0-9]+\.cloud\.couchbase\.com\b`)
29-
usernamePat = `?()/\+=\s\n`
30-
passwordPat = `^<>;.*&|£\n\s`
31-
// passwordPat = regexp.MustCompile(`(?i)(?:pass|pwd)(?:.|[\n\r]){0,15}(\b[^<>;.*&|£\n\s]{8,100}$)`)
32-
// passwordPat = regexp.MustCompile(`(?im)(?:pass|pwd)\S{0,40}?[:=\s]{1,3}[ '"=]{0,1}([^:?()/\+=\s\n]{4,40})\b`)
26+
connectionStringPat = regexp.MustCompile(`\b(cb\.[a-z0-9]+\.cloud\.couchbase\.com)\b`)
27+
usernamePat = common.UsernameRegexCheck(`?()/\+=\s\n`)
28+
passwordPat = common.PasswordRegexCheck(`^<>;.*&|£\n\s`)
3329
)
3430

35-
func meetsCouchbasePasswordRequirements(password string) (string, bool) {
36-
var hasLower, hasUpper, hasNumber, hasSpecialChar bool
37-
for _, char := range password {
38-
switch {
39-
case unicode.IsLower(char):
40-
hasLower = true
41-
case unicode.IsUpper(char):
42-
hasUpper = true
43-
case unicode.IsNumber(char):
44-
hasNumber = true
45-
case unicode.IsPunct(char) || unicode.IsSymbol(char):
46-
hasSpecialChar = true
47-
}
48-
49-
if hasLower && hasUpper && hasNumber && hasSpecialChar {
50-
return password, true
51-
}
52-
}
31+
func (s Scanner) Type() detectorspb.DetectorType {
32+
return detectorspb.DetectorType_Couchbase
33+
}
5334

54-
return "", false
35+
func (s Scanner) Description() string {
36+
return "Couchbase is a distributed NoSQL cloud database. Couchbase credentials can be used to access and modify data within the Couchbase database."
5537
}
5638

5739
// Keywords are used for efficiently pre-filtering chunks.
@@ -64,77 +46,37 @@ func (s Scanner) Keywords() []string {
6446
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
6547
dataStr := string(data)
6648

67-
connectionStringMatches := connectionStringPat.FindAllStringSubmatch(dataStr, -1)
49+
var uniqueConnStrings, uniqueUsernames, uniquePasswords = make(map[string]struct{}), make(map[string]struct{}), make(map[string]struct{})
6850

69-
// prepend 'couchbases://' to the connection string as the connection
70-
// string format is couchbases://cb.stuff.cloud.couchbase.com but the
71-
// cb.stuff.cloud.couchbase.com may be separated from the couchbases:// in codebases.
72-
for i, connectionStringMatch := range connectionStringMatches {
73-
connectionStringMatches[i][0] = "couchbases://" + connectionStringMatch[0]
51+
for _, match := range connectionStringPat.FindAllStringSubmatch(dataStr, -1) {
52+
uniqueConnStrings["couchbases://"+match[1]] = struct{}{}
7453
}
7554

76-
usernameRegexState := common.UsernameRegexCheck(usernamePat)
77-
usernameMatches := usernameRegexState.Matches(data)
78-
79-
passwordRegexState := common.PasswordRegexCheck(passwordPat)
80-
passwordMatches := passwordRegexState.Matches(data)
81-
82-
for _, connectionStringMatch := range connectionStringMatches {
83-
resConnectionStringMatch := strings.TrimSpace(connectionStringMatch[0])
84-
85-
for _, resUsernameMatch := range usernameMatches {
55+
for _, match := range usernamePat.Matches(data) {
56+
uniqueUsernames[match] = struct{}{}
57+
}
8658

87-
for _, resPasswordMatch := range passwordMatches {
88-
_, metPasswordRequirements := meetsCouchbasePasswordRequirements(resPasswordMatch)
59+
for _, match := range passwordPat.Matches(data) {
60+
uniquePasswords[match] = struct{}{}
61+
}
8962

90-
if !metPasswordRequirements {
63+
for connString := range uniqueConnStrings {
64+
for username := range uniqueUsernames {
65+
for password := range uniquePasswords {
66+
if !isValidCouchbasePassword(password) {
9167
continue
9268
}
9369

9470
s1 := detectors.Result{
9571
DetectorType: detectorspb.DetectorType_Couchbase,
96-
Raw: []byte(fmt.Sprintf("%s:%s@%s", resUsernameMatch, resPasswordMatch, resConnectionStringMatch)),
72+
Raw: fmt.Appendf([]byte(""), "%s:%s@%s", username, password, connString),
9773
}
9874

9975
if verify {
100-
101-
options := gocb.ClusterOptions{
102-
Authenticator: gocb.PasswordAuthenticator{
103-
Username: resUsernameMatch,
104-
Password: resPasswordMatch,
105-
},
106-
}
107-
108-
// Sets a pre-configured profile called "wan-development" to help avoid latency issues
109-
// when accessing Capella from a different Wide Area Network
110-
// or Availability Zone (e.g. your laptop).
111-
if err := options.ApplyProfile(gocb.ClusterConfigProfileWanDevelopment); err != nil {
112-
continue
113-
}
114-
115-
// Initialize the Connection
116-
cluster, err := gocb.Connect(resConnectionStringMatch, options)
117-
if err != nil {
118-
continue
119-
}
120-
121-
// We'll ping the KV nodes in our cluster.
122-
pings, err := cluster.Ping(&gocb.PingOptions{
123-
Timeout: time.Second * 5,
124-
})
125-
126-
if err != nil {
127-
continue
128-
}
129-
130-
for _, ping := range pings.Services {
131-
for _, pingEndpoint := range ping {
132-
if pingEndpoint.State == gocb.PingStateOk {
133-
s1.Verified = true
134-
break
135-
}
136-
}
137-
}
76+
isVerified, verificationErr := verifyCouchBase(username, password, connString)
77+
s1.Verified = isVerified
78+
s1.SetVerificationError(verificationErr)
79+
s1.SetPrimarySecretValue(connString)
13880
}
13981

14082
results = append(results, s1)
@@ -144,10 +86,62 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
14486
return results, nil
14587
}
14688

147-
func (s Scanner) Type() detectorspb.DetectorType {
148-
return detectorspb.DetectorType_Couchbase
89+
func verifyCouchBase(username, password, connString string) (bool, error) {
90+
options := gocb.ClusterOptions{
91+
Authenticator: gocb.PasswordAuthenticator{
92+
Username: username,
93+
Password: password,
94+
},
95+
}
96+
97+
// Sets a pre-configured profile called "wan-development" to help avoid latency issues
98+
// when accessing Capella from a different Wide Area Network
99+
// or Availability Zone (e.g. your laptop).
100+
if err := options.ApplyProfile(gocb.ClusterConfigProfileWanDevelopment); err != nil {
101+
return false, err
102+
}
103+
104+
// Initialize the Connection
105+
cluster, err := gocb.Connect(connString, options)
106+
if err != nil {
107+
return false, err
108+
}
109+
110+
// We'll ping the KV nodes in our cluster.
111+
pings, err := cluster.Ping(&gocb.PingOptions{
112+
Timeout: time.Second * 5,
113+
})
114+
115+
if err != nil {
116+
return false, err
117+
}
118+
119+
for _, ping := range pings.Services {
120+
for _, pingEndpoint := range ping {
121+
if pingEndpoint.State == gocb.PingStateOk {
122+
return true, nil
123+
}
124+
}
125+
}
126+
127+
return false, nil
149128
}
150129

151-
func (s Scanner) Description() string {
152-
return "Couchbase is a distributed NoSQL cloud database. Couchbase credentials can be used to access and modify data within the Couchbase database."
130+
func isValidCouchbasePassword(password string) bool {
131+
var hasLower, hasUpper, hasNumber, hasSpecialChar bool
132+
133+
for _, r := range password {
134+
switch {
135+
case unicode.IsLower(r):
136+
hasLower = true
137+
case unicode.IsUpper(r):
138+
hasUpper = true
139+
case unicode.IsNumber(r):
140+
hasNumber = true
141+
case unicode.IsPunct(r), unicode.IsSymbol(r):
142+
hasSpecialChar = true
143+
}
144+
}
145+
146+
return hasLower && hasUpper && hasNumber && hasSpecialChar
153147
}

pkg/detectors/couchbase/couchbase_integration_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import (
99
"testing"
1010
"time"
1111

12-
"github.com/kylelemons/godebug/pretty"
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/google/go-cmp/cmp/cmpopts"
1314
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
1415

1516
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
@@ -97,9 +98,9 @@ func TestCouchbase_FromChunk(t *testing.T) {
9798
if len(got[i].Raw) == 0 {
9899
t.Fatalf("no raw secret present: \n %+v", got[i])
99100
}
100-
got[i].Raw = nil
101101
}
102-
if diff := pretty.Compare(got, tt.want); diff != "" {
102+
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "primarySecret")
103+
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
103104
t.Errorf("Couchbase.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
104105
}
105106
})

pkg/detectors/couchbase/couchbase_test.go

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,6 @@ import (
1010
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
1111
)
1212

13-
var (
14-
validPattern = `
15-
# Configuration File: config.yaml
16-
database:
17-
host: $DB_HOST
18-
port: $DB_PORT
19-
username: $DB_USERNAME
20-
password: $DB_PASS # IMPORTANT: Do not share this password publicly
21-
22-
api:
23-
auth_type: "Password"
24-
in: "Configuration"
25-
couchbase_domain: "cb.testing.cloud.couchbase.com" // couchbase://
26-
couchbase_username: "usrpS@d>p"
27-
couchbase_password: "passwordU+2028 rf\@V[4,L/?2}"
28-
base_url: "https://$couchbase_domain/v1/user"
29-
30-
# Notes:
31-
# - Remember to rotate the secret every 90 days.
32-
# - The above credentials should only be used in a secure environment.
33-
`
34-
secrets = []string{
35-
`usrpS@d>p:passwordU+2028@couchbases://cb.testing.cloud.couchbase.com`,
36-
`$DB_USERNAME:passwordU+2028@couchbases://cb.testing.cloud.couchbase.com`,
37-
}
38-
)
39-
4013
func TestCouchBase_Pattern(t *testing.T) {
4114
d := Scanner{}
4215
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
@@ -47,9 +20,31 @@ func TestCouchBase_Pattern(t *testing.T) {
4720
want []string
4821
}{
4922
{
50-
name: "valid pattern",
51-
input: validPattern,
52-
want: secrets,
23+
name: "valid pattern",
24+
input: `
25+
# Configuration File: config.yaml
26+
database:
27+
host: $DB_HOST
28+
port: $DB_PORT
29+
username: $DB_USERNAME
30+
password: $DB_PASS # IMPORTANT: Do not share this password publicly
31+
32+
api:
33+
auth_type: "Password"
34+
in: "Configuration"
35+
couchbase_domain: "couchbases://cb.testing.cloud.couchbase.com"
36+
couchbase_username: "usrpS@d>p"
37+
couchbase_password: "passwordU+2028 rf\@V[4,L/?2}"
38+
base_url: "https://$couchbase_domain/v1/user"
39+
40+
# Notes:
41+
# - Remember to rotate the secret every 90 days.
42+
# - The above credentials should only be used in a secure environment.
43+
`,
44+
want: []string{
45+
"usrpS@d>p:passwordU+2028@couchbases://cb.testing.cloud.couchbase.com",
46+
"$DB_USERNAME:passwordU+2028@couchbases://cb.testing.cloud.couchbase.com",
47+
},
5348
},
5449
}
5550

0 commit comments

Comments
 (0)