-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* added godaddy detector * added pattern test cases * added integration test cases * resolved comments * added ote as keyword * added non secret pattern for negative test case
- Loading branch information
1 parent
dc789fc
commit 75e43bd
Showing
9 changed files
with
711 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
package godaddy | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
regexp "github.com/wasilibs/go-re2" | ||
|
||
"github.com/trufflesecurity/trufflehog/v3/pkg/common" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" | ||
) | ||
|
||
type Scanner struct { | ||
client *http.Client | ||
} | ||
|
||
var ( | ||
// ensure the scanner satisfies the interface at compile time. | ||
_ detectors.Detector = (*Scanner)(nil) | ||
_ detectors.Versioner = (*Scanner)(nil) | ||
|
||
defaultClient = common.SaneHttpClient() | ||
|
||
// the key for the GoDaddy OTE environment is a 37-character alphanumeric string that may include underscores. | ||
keyPattern = regexp.MustCompile(detectors.PrefixRegex([]string{"godaddy", "ote"}) + common.BuildRegex("a-zA-Z0-9", "_", 37)) | ||
// the secret for the GoDaddy OTE environment is a 22-character alphanumeric string. | ||
secretPattern = regexp.MustCompile(detectors.PrefixRegex([]string{"godaddy", "ote"}) + common.BuildRegex("a-zA-Z0-9", "", 22)) | ||
|
||
// ote environment | ||
ote = "api.ote-godaddy.com" | ||
) | ||
|
||
func (s *Scanner) getClient() *http.Client { | ||
if s.client != nil { | ||
return s.client | ||
} | ||
|
||
return defaultClient | ||
} | ||
|
||
func (s *Scanner) Version() int { return 1 } | ||
|
||
// Keywords are used for efficiently pre-filtering chunks. | ||
// Use identifiers in the secret preferably, or the provider name. | ||
func (s Scanner) Keywords() []string { | ||
return []string{"godaddy", "ote"} | ||
} | ||
|
||
func (s Scanner) Description() string { | ||
return "GoDaddy offers website building, hosting and security tools and services to construct, expand and protect the online presence." + | ||
"GoDaddy provides applications and access to relevant third-party products and platforms to connect their customers" | ||
} | ||
|
||
func (s Scanner) Type() detectorspb.DetectorType { | ||
return detectorspb.DetectorType_GoDaddy | ||
} | ||
|
||
// FromData will find and optionally verify GoDaddy API Key and secrets in a given set of bytes. | ||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { | ||
// convert the data to string | ||
dataStr := string(data) | ||
|
||
// find all the matching keys and secret in data and make a unique maps of both keys and secret. | ||
uniqueKeys, uniqueSecrets := make(map[string]struct{}), make(map[string]struct{}) | ||
|
||
for _, foundKey := range keyPattern.FindAllStringSubmatch(dataStr, -1) { | ||
uniqueKeys[foundKey[1]] = struct{}{} | ||
} | ||
|
||
for _, foundSecret := range secretPattern.FindAllStringSubmatch(dataStr, -1) { | ||
uniqueSecrets[foundSecret[1]] = struct{}{} | ||
} | ||
|
||
for key := range uniqueKeys { | ||
for secret := range uniqueSecrets { | ||
result := detectors.Result{ | ||
DetectorType: detectorspb.DetectorType_GoDaddy, | ||
Raw: []byte(key), | ||
ExtraData: make(map[string]string), | ||
} | ||
|
||
if verify { | ||
isVerified, verificationErr := VerifyGoDaddySecret(ctx, s.getClient(), ote, MakeAuthHeaderValue(key, secret)) | ||
|
||
result.Verified = isVerified | ||
result.SetVerificationError(verificationErr, secret) | ||
|
||
// in case of successful verification add the enviorement name in extradata to let user know which env this secret belong to. | ||
if isVerified { | ||
result.ExtraData["Environment"] = "OTE" | ||
} | ||
} | ||
|
||
results = append(results, result) | ||
} | ||
} | ||
|
||
return results, nil | ||
|
||
} | ||
|
||
// VerifyGoDaddySecret make a call to godaddy api with given secret to check if secret is valid or not. | ||
func VerifyGoDaddySecret(ctx context.Context, client *http.Client, environment, secret string) (bool, error) { | ||
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://%s/v1/domains/available?domain=example.com", environment), http.NoBody) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
// set the required auth header | ||
req.Header.Set("Authorization", secret) | ||
|
||
resp, err := client.Do(req) | ||
if err != nil { | ||
return false, err | ||
} | ||
defer func() { | ||
_, _ = io.Copy(io.Discard, resp.Body) | ||
_ = resp.Body.Close() | ||
}() | ||
|
||
switch resp.StatusCode { | ||
case http.StatusOK: | ||
return true, nil | ||
case http.StatusUnauthorized: | ||
return false, nil | ||
case http.StatusForbidden: | ||
// as per documentation in case of 403 the token is actually verified but it does not have access. | ||
return true, nil | ||
default: | ||
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) | ||
} | ||
} | ||
|
||
// MakeAuthHeaderValue return a value made from key and secret that can be used as authorization header value for godaddy API's. | ||
func MakeAuthHeaderValue(key, secret string) string { | ||
return fmt.Sprintf("sso-key %s:%s", key, secret) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
//go:build detectors | ||
// +build detectors | ||
|
||
package godaddy | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"testing" | ||
"time" | ||
|
||
"github.com/kylelemons/godebug/pretty" | ||
|
||
"github.com/trufflesecurity/trufflehog/v3/pkg/common" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" | ||
) | ||
|
||
func TestGoDaddy_FromChunk(t *testing.T) { | ||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) | ||
defer cancel() | ||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5") | ||
if err != nil { | ||
t.Fatalf("could not get test secrets from GCP: %s", err) | ||
} | ||
secret := testSecrets.MustGetField("GODADDY_OTE") | ||
inactiveSecret := testSecrets.MustGetField("GODADDY_OTE_INACTIVE") | ||
|
||
type args struct { | ||
ctx context.Context | ||
data []byte | ||
verify bool | ||
} | ||
tests := []struct { | ||
name string | ||
s Scanner | ||
args args | ||
want []detectors.Result | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "found, verified", | ||
s: Scanner{}, | ||
args: args{ | ||
ctx: context.Background(), | ||
data: []byte(fmt.Sprintf("You can find a godaddy secret %s within", secret)), | ||
verify: true, | ||
}, | ||
want: []detectors.Result{ | ||
{ | ||
DetectorType: detectorspb.DetectorType_GoDaddy, | ||
Verified: true, | ||
}, | ||
}, | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "found, unverified", | ||
s: Scanner{}, | ||
args: args{ | ||
ctx: context.Background(), | ||
data: []byte(fmt.Sprintf("You can find a godaddy secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation | ||
verify: true, | ||
}, | ||
want: []detectors.Result{ | ||
{ | ||
DetectorType: detectorspb.DetectorType_GoDaddy, | ||
Verified: false, | ||
}, | ||
}, | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "not found", | ||
s: Scanner{}, | ||
args: args{ | ||
ctx: context.Background(), | ||
data: []byte("You cannot find the secret within"), | ||
verify: true, | ||
}, | ||
want: nil, | ||
wantErr: false, | ||
}, | ||
} | ||
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) | ||
if (err != nil) != tt.wantErr { | ||
t.Errorf("GoDaddy.FromData() error = %v, wantErr %v", err, tt.wantErr) | ||
return | ||
} | ||
for i := range got { | ||
if len(got[i].Raw) == 0 { | ||
t.Fatalf("no raw secret present: \n %+v", got[i]) | ||
} | ||
got[i].Raw = nil | ||
} | ||
if diff := pretty.Compare(got, tt.want); diff != "" { | ||
t.Errorf("GoDaddy.FromData() %s diff: (-got +want)\n%s", tt.name, diff) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func BenchmarkFromData(benchmark *testing.B) { | ||
ctx := context.Background() | ||
s := Scanner{} | ||
for name, data := range detectors.MustGetBenchmarkData() { | ||
benchmark.Run(name, func(b *testing.B) { | ||
b.ResetTimer() | ||
for n := 0; n < b.N; n++ { | ||
_, err := s.FromData(ctx, false, data) | ||
if err != nil { | ||
b.Fatal(err) | ||
} | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package godaddy | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
|
||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick" | ||
) | ||
|
||
var ( | ||
validPattern = `[{ | ||
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5", | ||
"name": "GoDaddy", | ||
"type": "Detector", | ||
"api": true, | ||
"authentication_type": "", | ||
"verification_url": "https://api.example.com/example", | ||
"test_secrets": { | ||
"godaddyKey": "2TM44WqB21o4zH_3xM44WkB21i4zHHhXSoHjO", | ||
"godaddySecret": "3xM44WkB21i4zHHhXSoHjO", | ||
"not_godaddySecret": "2TM44WqB21o4zH$3xM44WkB21i4zHHhXSoHjO" | ||
}, | ||
"expected_response": "200", | ||
"method": "GET", | ||
"deprecated": false | ||
}]` | ||
secret = "2TM44WqB21o4zH_3xM44WkB21i4zHHhXSoHjO" | ||
) | ||
|
||
func TestGoDaddy_Pattern(t *testing.T) { | ||
d := Scanner{} | ||
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d}) | ||
|
||
tests := []struct { | ||
name string | ||
input string | ||
want []string | ||
}{ | ||
{ | ||
name: "valid pattern", | ||
input: validPattern, | ||
want: []string{secret}, | ||
}, | ||
} | ||
|
||
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) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.