Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions pkg/detectors/gcpoauth2/gcpoauth2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package gcpoauth2

import (
"context"
"strings"

regexp "github.com/wasilibs/go-re2"
"golang.org/x/oauth2/clientcredentials"
"golang.org/x/oauth2/google"

"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)

type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}

var _ detectors.Detector = (*Scanner)(nil)

var (
oauth2ClientID = regexp.MustCompile("[0-9a-zA-Z\\-_]{16,}\\.apps\\.googleusercontent\\.com")
oauth2ClientSecret = regexp.MustCompile("GOCSPX-[0-9a-zA-Z\\-_]{20,}")
)

const (
gcpOAuthBadVerificationCodeError = "bad_verification_code"
)

func (s Scanner) Keywords() []string {
return []string{".apps.googleusercontent.com", "GOCSPX-", "oauth2_client_id", "oauth2_client_secret"}
}

func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_GoogleOauth2
}

func (s Scanner) Description() string {
return "GCP OAuth2 credentials are sensitive strings (client ID and secret) issued by Google Cloud to identify your application and securely authorize its access to Google APIs on behalf of users."
}

func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data)

oauth2ClientIDMatches := oauth2ClientID.FindAllStringSubmatch(dataStr, -1)
oauth2ClientSecretMatches := oauth2ClientSecret.FindAllStringSubmatch(dataStr, -1)

seen := make(map[string]bool)

pairedIDs := make(map[string]bool)
pairedSecrets := make(map[string]bool)

if len(oauth2ClientIDMatches) > 0 && len(oauth2ClientSecretMatches) > 0 {
for _, idMatch := range oauth2ClientIDMatches {
for _, secretMatch := range oauth2ClientSecretMatches {
clientID := idMatch[0]
clientSecret := secretMatch[0]
key := "pair:" + clientID + ":" + clientSecret

if !seen[key] {
seen[key] = true
pairedIDs[clientID] = true
pairedSecrets[clientSecret] = true

s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_GoogleOauth2,
Raw: []byte(clientID),
RawV2: []byte(clientID + clientSecret),
}

if verify {
config := &clientcredentials.Config{
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: google.Endpoint.TokenURL,
}
_, err := config.Token(ctx)
if err != nil && strings.Contains(err.Error(), gcpOAuthBadVerificationCodeError) {
s1.Verified = true
}
}

results = append(results, s1)
}
}
}
}

// Process orphan ClientID-only matches (not part of any pair)
if len(oauth2ClientIDMatches) > 0 && len(pairedIDs) == 0 {
for _, idMatch := range oauth2ClientIDMatches {
clientID := idMatch[0]
key := "id:" + clientID

if !pairedIDs[clientID] && !seen[key] {
seen[key] = true
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_GoogleOauth2,
Raw: []byte(clientID),
RawV2: []byte(clientID),
}
results = append(results, s1)
}
}
}

// Process orphan ClientSecret-only matches (not part of any pair)
if len(oauth2ClientSecretMatches) > 0 && len(pairedSecrets) == 0 {
for _, secretMatch := range oauth2ClientSecretMatches {
clientSecret := secretMatch[0]
key := "secret:" + clientSecret

if !pairedSecrets[clientSecret] && !seen[key] {
seen[key] = true
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_GoogleOauth2,
Raw: []byte(clientSecret),
RawV2: []byte(clientSecret),
}
results = append(results, s1)
}
}
}
return
}
96 changes: 96 additions & 0 deletions pkg/detectors/gcpoauth2/gcpoauth2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package gcpoauth2

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"

"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)

func TestGcpOAuth2_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "typical pattern - with keyword oauth2_client_id",
input: "oauth2_client_id = '1234567890-abc123def456ghi789jkl012mno345pq.apps.googleusercontent.com'",
want: []string{
"1234567890-abc123def456ghi789jkl012mno345pq.apps.googleusercontent.com",
},
},
{
name: "typical pattern - with keyword oauth2_client_secret",
input: "oauth2_client_secret = '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'",
want: []string{
"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",
},
},
{
name: "typical pattern - multiline with both keywords",
input: `oauth2_client_id = '1234567890-abc123def456ghi789jkl012mno345pq.apps.googleusercontent.com'
oauth2_client_secret = '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'`,
want: []string{
"1234567890-abc123def456ghi789jkl012mno345pq.apps.googleusercontent.comGOCSPX-5aBcD3fgHiJK_lMnOpQRstuVwXyZ",
},
},
{
name: "typical pattern - invalid client secret",
input: "oauth2_client_secret = 'GOCCCX-5aBcD3fgHiJK_lMnOpQRstuVwXyZ'",
want: nil,
},
{
name: "typical pattern - invalid client ID",
input: "oauth2_client_id = '1234567890-abc123def456ghi789jkl0gmail.com'",
want: nil,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
return
}

results, err := d.FromData(context.Background(), false, []byte(test.input))
if err != nil {
t.Errorf("error = %v", err)
return
}

if len(results) != len(test.want) {
if len(results) == 0 {
t.Errorf("did not receive result")
} else {
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
}
return
}

actual := make(map[string]struct{}, len(results))
for _, r := range results {
if len(r.RawV2) > 0 {
actual[string(r.RawV2)] = struct{}{}
} else {
actual[string(r.Raw)] = struct{}{}
}
}
expected := make(map[string]struct{}, len(test.want))
for _, v := range test.want {
expected[v] = struct{}{}
}

if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
}
})
}
}
Loading