Merge remote-tracking branch 'upstream/main' into detector-rootly
Signed-off-by: Sahil Silare <[email protected]>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"math/big"
|
||||
mrand "math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -70,3 +71,65 @@ func SliceContainsString(origTargetString string, stringSlice []string, ignoreCa
|
||||
}
|
||||
return false, "", 0
|
||||
}
|
||||
|
||||
// GoFakeIt Password generator does not guarantee inclusion of characters.
|
||||
// Using a custom random password generator with guaranteed inclusions (atleast) of lower, upper, numeric and special characters
|
||||
func GenerateRandomPassword(lower, upper, numeric, special bool, length int) string {
|
||||
if length < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var password []rune
|
||||
var required []rune
|
||||
var allowed []rune
|
||||
|
||||
lowerChars := []rune("abcdefghijklmnopqrstuvwxyz")
|
||||
upperChars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
specialChars := []rune("!@#$%^&*()-_=+[]{}|;:',.<>?/")
|
||||
numberChars := []rune("0123456789")
|
||||
|
||||
// Ensure inclusion from each requested category
|
||||
if lower {
|
||||
rand, _ := rand.Int(rand.Reader, big.NewInt(int64(len(lowerChars))))
|
||||
ch := lowerChars[rand.Int64()]
|
||||
required = append(required, ch)
|
||||
allowed = append(allowed, lowerChars...)
|
||||
}
|
||||
if upper {
|
||||
rand, _ := rand.Int(rand.Reader, big.NewInt(int64(len(upperChars))))
|
||||
ch := upperChars[rand.Int64()]
|
||||
required = append(required, ch)
|
||||
allowed = append(allowed, upperChars...)
|
||||
}
|
||||
if numeric {
|
||||
rand, _ := rand.Int(rand.Reader, big.NewInt(int64(len(numberChars))))
|
||||
ch := numberChars[rand.Int64()]
|
||||
required = append(required, ch)
|
||||
allowed = append(allowed, numberChars...)
|
||||
}
|
||||
if special {
|
||||
rand, _ := rand.Int(rand.Reader, big.NewInt(int64(len(specialChars))))
|
||||
ch := specialChars[rand.Int64()]
|
||||
required = append(required, ch)
|
||||
allowed = append(allowed, specialChars...)
|
||||
}
|
||||
|
||||
if len(allowed) == 0 {
|
||||
return "" // No character sets enabled
|
||||
}
|
||||
|
||||
// Fill the rest of the password
|
||||
for i := 0; i < length-len(required); i++ {
|
||||
rand, _ := rand.Int(rand.Reader, big.NewInt(int64(len(allowed))))
|
||||
ch := allowed[rand.Int64()]
|
||||
password = append(password, ch)
|
||||
}
|
||||
|
||||
// Combine required and random characters, then shuffle
|
||||
password = append(password, required...)
|
||||
mrand.Shuffle(len(password), func(i, j int) {
|
||||
password[i], password[j] = password[j], password[i]
|
||||
})
|
||||
|
||||
return string(password)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func TestAddItem(t *testing.T) {
|
||||
@@ -194,3 +195,85 @@ func TestSliceContainsString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRandomPassword_Length(t *testing.T) {
|
||||
pass := GenerateRandomPassword(true, true, true, true, 16)
|
||||
if len(pass) != 16 {
|
||||
t.Errorf("expected length 16, got %d", len(pass))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRandomPassword_Empty(t *testing.T) {
|
||||
pass := GenerateRandomPassword(false, false, false, false, 10)
|
||||
if pass != "" {
|
||||
t.Errorf("expected empty string, got %q", pass)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRandomPassword_RequiredSets(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
lower bool
|
||||
upper bool
|
||||
numeric bool
|
||||
special bool
|
||||
}{
|
||||
{"lower only", true, false, false, false},
|
||||
{"upper only", false, true, false, false},
|
||||
{"numeric only", false, false, true, false},
|
||||
{"special only", false, false, false, true},
|
||||
{"all", true, true, true, true},
|
||||
{"lower+upper", true, true, false, false},
|
||||
{"lower+numeric", true, false, true, false},
|
||||
{"upper+special", false, true, false, true},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
pass := GenerateRandomPassword(tc.lower, tc.upper, tc.numeric, tc.special, 12)
|
||||
if len(pass) != 12 {
|
||||
t.Errorf("expected length 12, got %d", len(pass))
|
||||
}
|
||||
if tc.lower && !contains(pass, unicode.IsLower) {
|
||||
t.Errorf("expected at least one lowercase letter")
|
||||
}
|
||||
if tc.upper && !contains(pass, unicode.IsUpper) {
|
||||
t.Errorf("expected at least one uppercase letter")
|
||||
}
|
||||
if tc.numeric && !contains(pass, unicode.IsDigit) {
|
||||
t.Errorf("expected at least one digit")
|
||||
}
|
||||
if tc.special && !containsSpecial(pass) {
|
||||
t.Errorf("expected at least one special character")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRandomPassword_ShortLength(t *testing.T) {
|
||||
pass := GenerateRandomPassword(true, true, true, true, 0)
|
||||
if pass != "" {
|
||||
t.Errorf("expected empty string for length 0, got %q", pass)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s string, fn func(rune) bool) bool {
|
||||
for _, r := range s {
|
||||
if fn(r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsSpecial(s string) bool {
|
||||
specials := "!@#$%^&*()-_=+[]{}|;:',.<>?/"
|
||||
for _, r := range s {
|
||||
for _, sr := range specials {
|
||||
if r == sr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3,14 +3,18 @@ package algoliaadminkey
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
|
||||
)
|
||||
@@ -28,6 +32,10 @@ var (
|
||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"algolia", "docsearch", "appId"}) + `\b([A-Z0-9]{10})\b`)
|
||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"algolia", "docsearch", "apiKey"}) + `\b([a-zA-Z0-9]{32})\b`)
|
||||
|
||||
invalidHosts = simple.NewCache[struct{}]()
|
||||
|
||||
errNoHost = errors.New("no such host")
|
||||
)
|
||||
|
||||
// Keywords are used for efficiently pre-filtering chunks.
|
||||
@@ -38,6 +46,7 @@ func (s Scanner) Keywords() []string {
|
||||
|
||||
// FromData will find and optionally verify AlgoliaAdminKey secrets in a given set of bytes.
|
||||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
||||
logger := logContext.AddLogger(ctx).Logger().WithName("algoliaadminkey")
|
||||
dataStr := string(data)
|
||||
|
||||
// Deduplicate matches.
|
||||
@@ -59,6 +68,12 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
// Test matches.
|
||||
for key := range keyMatches {
|
||||
for id := range idMatches {
|
||||
if invalidHosts.Exists(id) {
|
||||
logger.V(3).Info("Skipping application id: no such host", "host", id)
|
||||
delete(idMatches, id)
|
||||
continue
|
||||
}
|
||||
|
||||
r := detectors.Result{
|
||||
DetectorType: detectorspb.DetectorType_AlgoliaAdminKey,
|
||||
Raw: []byte(key),
|
||||
@@ -70,7 +85,14 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
isVerified, extraData, verificationErr := verifyMatch(ctx, id, key)
|
||||
r.Verified = isVerified
|
||||
r.ExtraData = extraData
|
||||
r.SetVerificationError(verificationErr, key)
|
||||
if verificationErr != nil {
|
||||
if errors.Is(verificationErr, errNoHost) {
|
||||
invalidHosts.Set(id, struct{}{})
|
||||
continue
|
||||
}
|
||||
|
||||
r.SetVerificationError(verificationErr, key)
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, r)
|
||||
@@ -101,6 +123,11 @@ func verifyMatch(ctx context.Context, appId, apiKey string) (bool, map[string]st
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
// lookup xyz.algolia.net: no such host
|
||||
if strings.Contains(err.Error(), "no such host") {
|
||||
return false, nil, errNoHost
|
||||
}
|
||||
|
||||
return false, nil, err
|
||||
}
|
||||
defer func() {
|
||||
|
||||
@@ -2,12 +2,15 @@ package artifactory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
|
||||
)
|
||||
@@ -26,14 +29,20 @@ var (
|
||||
defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses
|
||||
|
||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||
keyPat = regexp.MustCompile(`\b([a-zA-Z0-9]{73}|\b[a-zA-Z0-9]{64})`)
|
||||
URLPat = regexp.MustCompile(`\b([A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])\.jfrog\.io)`)
|
||||
keyPat = regexp.MustCompile(`\b([a-zA-Z0-9]{64,73})\b`)
|
||||
URLPat = regexp.MustCompile(`\b([A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]\.jfrog\.io)`)
|
||||
|
||||
invalidHosts = simple.NewCache[struct{}]()
|
||||
|
||||
errNoHost = errors.New("no such host")
|
||||
)
|
||||
|
||||
func (Scanner) CloudEndpoint() string { return "" }
|
||||
|
||||
// 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{"artifactory"}
|
||||
return []string{"artifactory", "jfrog.io"}
|
||||
}
|
||||
|
||||
func (s Scanner) getClient() *http.Client {
|
||||
@@ -46,30 +55,50 @@ func (s Scanner) getClient() *http.Client {
|
||||
// FromData will find and optionally verify Artifactory secrets in a given set of bytes.
|
||||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
||||
dataStr := string(data)
|
||||
URLmatches := URLPat.FindAllStringSubmatch(dataStr, -1)
|
||||
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
|
||||
|
||||
resURLMatch := ""
|
||||
for _, URLmatch := range URLmatches {
|
||||
resURLMatch = strings.TrimSpace(URLmatch[1])
|
||||
var uniqueTokens, uniqueUrls = make(map[string]struct{}), make(map[string]struct{})
|
||||
|
||||
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
|
||||
uniqueTokens[match[1]] = struct{}{}
|
||||
}
|
||||
|
||||
for _, match := range matches {
|
||||
resMatch := strings.TrimSpace(match[1])
|
||||
var foundUrls = make([]string, 0)
|
||||
|
||||
client := s.getClient()
|
||||
for _, match := range URLPat.FindAllStringSubmatch(dataStr, -1) {
|
||||
foundUrls = append(foundUrls, match[1])
|
||||
}
|
||||
|
||||
// add found + configured endpoints to the list
|
||||
for _, endpoint := range s.Endpoints(foundUrls...) {
|
||||
// if any configured endpoint has `https://` remove it because we append that during verification
|
||||
endpoint = strings.TrimPrefix(endpoint, "https://")
|
||||
uniqueUrls[endpoint] = struct{}{}
|
||||
}
|
||||
|
||||
for token := range uniqueTokens {
|
||||
for url := range uniqueUrls {
|
||||
if invalidHosts.Exists(url) {
|
||||
delete(uniqueUrls, url)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, URL := range s.Endpoints(resURLMatch) {
|
||||
s1 := detectors.Result{
|
||||
DetectorType: detectorspb.DetectorType_ArtifactoryAccessToken,
|
||||
Raw: []byte(resMatch),
|
||||
RawV2: []byte(resMatch + URL),
|
||||
Raw: []byte(token),
|
||||
RawV2: []byte(token + url),
|
||||
}
|
||||
|
||||
if verify {
|
||||
isVerified, verificationErr := verifyArtifactory(ctx, client, URL, resMatch)
|
||||
isVerified, verificationErr := verifyArtifactory(ctx, s.getClient(), url, token)
|
||||
s1.Verified = isVerified
|
||||
s1.SetVerificationError(verificationErr, resMatch)
|
||||
if verificationErr != nil {
|
||||
if errors.Is(verificationErr, errNoHost) {
|
||||
invalidHosts.Set(url, struct{}{})
|
||||
continue
|
||||
}
|
||||
|
||||
s1.SetVerificationError(verificationErr, token)
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, s1)
|
||||
@@ -81,26 +110,45 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
}
|
||||
|
||||
func verifyArtifactory(ctx context.Context, client *http.Client, resURLMatch, resMatch string) (bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://"+resURLMatch+"/artifactory/api/storageinfo", nil)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://"+resURLMatch+"/artifactory/api/system/ping", nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
req.Header.Add("X-JFrog-Art-Api", resMatch)
|
||||
res, err := client.Do(req)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// lookup foo.jfrog.io: no such host
|
||||
if strings.Contains(err.Error(), "no such host") {
|
||||
return false, errNoHost
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
switch res.StatusCode {
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return true, nil
|
||||
case http.StatusForbidden:
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if strings.Contains(string(body), "OK") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden, http.StatusFound: // 302 can occur if the url is incorrect
|
||||
// https://jfrog.com/help/r/jfrog-rest-apis/error-responses
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
|
||||
return false, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,42 +56,6 @@ func TestArtifactory_FromChunk(t *testing.T) {
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, real secrets, verification error due to timeout",
|
||||
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a artifactory secret %s and domain %s but not verified", secret, appURL)),
|
||||
verify: true,
|
||||
},
|
||||
want: func() []detectors.Result {
|
||||
r := detectors.Result{
|
||||
DetectorType: detectorspb.DetectorType_ArtifactoryAccessToken,
|
||||
Verified: false,
|
||||
}
|
||||
r.SetVerificationError(context.DeadlineExceeded)
|
||||
return []detectors.Result{r}
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, real secrets, verification error due to unexpected api surface",
|
||||
s: Scanner{client: common.ConstantResponseHttpClient(500, "{}")},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a artifactory secret %s and domain %s but not verified", secret, appURL)),
|
||||
verify: true,
|
||||
},
|
||||
want: func() []detectors.Result {
|
||||
r := detectors.Result{
|
||||
DetectorType: detectorspb.DetectorType_ArtifactoryAccessToken,
|
||||
Verified: false,
|
||||
}
|
||||
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 500"))
|
||||
return []detectors.Result{r}
|
||||
}(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, unverified",
|
||||
s: Scanner{},
|
||||
@@ -122,6 +86,8 @@ func TestArtifactory_FromChunk(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.s.UseFoundEndpoints(true)
|
||||
|
||||
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Artifactory.FromData() error = %v, wantErr %v", err, tt.wantErr)
|
||||
@@ -143,7 +109,7 @@ func TestArtifactory_FromChunk(t *testing.T) {
|
||||
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.want[i].VerificationError(), got[i].VerificationError())
|
||||
}
|
||||
}
|
||||
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError")
|
||||
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "primarySecret")
|
||||
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
|
||||
t.Errorf("Artifactory.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package artifactory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -11,13 +10,6 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
|
||||
)
|
||||
|
||||
var (
|
||||
validPattern = "5YcZhIKwxTdxwpZHf9c1Usu8xNtAklRsqWYXWf2qmjW0RSQQ0U4sVnrNgOwIJlTOqJf06T3dl / RVo8ytzB65L.jfrog.io"
|
||||
// validPattern2 is for cloud endpoints so it does not have any JFrog endpoint
|
||||
validPattern2 = "5YcZhIKwxTdxwpZHf9c1Usu8xNtAklRsqWYXWf2qmjW0RSQQ0U4sVnrNgOwIJlTOqJf06T3dl"
|
||||
invalidPattern = "W0RSQQ0U4sVnrNgOwIJlTOqJf06T3dl^&5YcZhIKwxTdxwpZHf9c1Usu8xNtA / rtest#y$zB65L%.jfrog.io"
|
||||
)
|
||||
|
||||
func TestArtifactory_Pattern(t *testing.T) {
|
||||
d := Scanner{}
|
||||
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
|
||||
@@ -31,23 +23,76 @@ func TestArtifactory_Pattern(t *testing.T) {
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "valid pattern",
|
||||
input: fmt.Sprintf("artifactory credentials: %s", validPattern),
|
||||
name: "valid pattern",
|
||||
input: `
|
||||
# artifactory credentials
|
||||
Token: cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZg
|
||||
Url: rwxtOp.jfrog.io
|
||||
`,
|
||||
useCloudEndpoint: false,
|
||||
useFoundEndpoint: true,
|
||||
want: []string{"5YcZhIKwxTdxwpZHf9c1Usu8xNtAklRsqWYXWf2qmjW0RSQQ0U4sVnrNgOwIJlTOqJf06T3dlRVo8ytzB65L.jfrog.io"},
|
||||
want: []string{"cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZgrwxtOp.jfrog.io"},
|
||||
},
|
||||
{
|
||||
name: "valid pattern - with cloud endpoints",
|
||||
input: fmt.Sprintf("artifactory credentials: %s", validPattern2),
|
||||
name: "valid pattern - with cloud endpoints",
|
||||
input: `
|
||||
# artifactory credentials
|
||||
Token: cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZg
|
||||
`,
|
||||
cloudEndpoint: "cloudendpoint.jfrog.io",
|
||||
useCloudEndpoint: true,
|
||||
useFoundEndpoint: false,
|
||||
want: []string{"5YcZhIKwxTdxwpZHf9c1Usu8xNtAklRsqWYXWf2qmjW0RSQQ0U4sVnrNgOwIJlTOqJf06T3dlcloudendpoint.jfrog.io"},
|
||||
want: []string{"cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZgcloudendpoint.jfrog.io"},
|
||||
},
|
||||
{
|
||||
name: "invalid pattern",
|
||||
input: fmt.Sprintf("artifactory credentials: %s", invalidPattern),
|
||||
name: "valid pattern - with cloud and found endpoints",
|
||||
input: `
|
||||
# artifactory credentials
|
||||
Token: cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZg
|
||||
Url: rwxtOp.jfrog.io
|
||||
`,
|
||||
cloudEndpoint: "cloudendpoint.jfrog.io",
|
||||
useCloudEndpoint: true,
|
||||
useFoundEndpoint: true,
|
||||
want: []string{
|
||||
"cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZgcloudendpoint.jfrog.io",
|
||||
"cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZgrwxtOp.jfrog.io",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid pattern - with disabled found endpoints",
|
||||
input: `
|
||||
# artifactory credentials
|
||||
Token: cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZg
|
||||
Url: rwxtOp.jfrog.io
|
||||
`,
|
||||
cloudEndpoint: "cloudendpoint.jfrog.io",
|
||||
useCloudEndpoint: true,
|
||||
useFoundEndpoint: false,
|
||||
want: []string{
|
||||
"cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZgcloudendpoint.jfrog.io",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid pattern - with https in configured endpoint",
|
||||
input: `
|
||||
# artifactory credentials
|
||||
Token: cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZg
|
||||
`,
|
||||
cloudEndpoint: "https://cloudendpoint.jfrog.io",
|
||||
useCloudEndpoint: true,
|
||||
useFoundEndpoint: false,
|
||||
want: []string{
|
||||
"cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZgcloudendpoint.jfrog.io",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid pattern",
|
||||
input: `
|
||||
# artifactory credentials
|
||||
Token: cmVmdGtuOjAxOjE3ODA_NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZg
|
||||
Url: rwxtOp.jfroq.io
|
||||
`,
|
||||
useFoundEndpoint: true,
|
||||
want: nil,
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ package billomat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -25,8 +26,10 @@ var (
|
||||
client = common.SaneHttpClient()
|
||||
|
||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"billomat"}) + `\b([0-9a-z]{32})\b`)
|
||||
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"billomat"}) + `\b([0-9a-z]{1,})\b`)
|
||||
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"billomat"}) + `\b([0-9a-z]{4,20})\b`) // the Billomat ID must be between 4 and 20 characters long.
|
||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"billomat"}) + `\b([0-9a-f]{32})\b`)
|
||||
|
||||
errAccountIDNotFound = errors.New("account id not found")
|
||||
)
|
||||
|
||||
// Keywords are used for efficiently pre-filtering chunks.
|
||||
@@ -35,37 +38,6 @@ func (s Scanner) Keywords() []string {
|
||||
return []string{"billomat"}
|
||||
}
|
||||
|
||||
// FromData will find and optionally verify Billomat secrets in a given set of bytes.
|
||||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
||||
dataStr := string(data)
|
||||
|
||||
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
|
||||
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)
|
||||
|
||||
for _, match := range matches {
|
||||
resMatch := strings.TrimSpace(match[1])
|
||||
for _, idMatch := range idMatches {
|
||||
resId := strings.TrimSpace(idMatch[1])
|
||||
|
||||
s1 := detectors.Result{
|
||||
DetectorType: detectorspb.DetectorType_Billomat,
|
||||
Raw: []byte(resMatch),
|
||||
RawV2: []byte(resMatch + resId),
|
||||
}
|
||||
|
||||
if verify {
|
||||
isVerified, verificationErr := verifyBillomat(ctx, client, resId, resMatch)
|
||||
s1.Verified = isVerified
|
||||
s1.SetVerificationError(verificationErr, resMatch)
|
||||
}
|
||||
|
||||
results = append(results, s1)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s Scanner) Type() detectorspb.DetectorType {
|
||||
return detectorspb.DetectorType_Billomat
|
||||
}
|
||||
@@ -74,9 +46,52 @@ func (s Scanner) Description() string {
|
||||
return "Billomat is an online invoicing software. Billomat API keys can be used to access and manage invoices, clients, and other related data."
|
||||
}
|
||||
|
||||
// FromData will find and optionally verify Billomat secrets in a given set of bytes.
|
||||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
||||
dataStr := string(data)
|
||||
|
||||
var uniqueIDs, uniqueAPIKeys = make(map[string]struct{}), make(map[string]struct{})
|
||||
|
||||
for _, match := range idPat.FindAllStringSubmatch(dataStr, -1) {
|
||||
uniqueIDs[match[1]] = struct{}{}
|
||||
}
|
||||
|
||||
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
|
||||
uniqueAPIKeys[match[1]] = struct{}{}
|
||||
}
|
||||
|
||||
for apiKey := range uniqueAPIKeys {
|
||||
for id := range uniqueIDs {
|
||||
s1 := detectors.Result{
|
||||
DetectorType: detectorspb.DetectorType_Billomat,
|
||||
Raw: []byte(apiKey),
|
||||
RawV2: []byte(apiKey + id),
|
||||
}
|
||||
|
||||
if verify {
|
||||
isVerified, verificationErr := verifyBillomat(ctx, client, id, apiKey)
|
||||
s1.Verified = isVerified
|
||||
if verificationErr != nil {
|
||||
// remove the account ID if not found to prevent reuse during other API key checks.
|
||||
if errors.Is(verificationErr, errAccountIDNotFound) {
|
||||
delete(uniqueIDs, id)
|
||||
continue
|
||||
}
|
||||
|
||||
s1.SetVerificationError(verificationErr, apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, s1)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// docs: https://www.billomat.com/en/api/basics/authentication/
|
||||
func verifyBillomat(ctx context.Context, client *http.Client, id, key string) (bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://%s.billomat.net/api/v2/clients/myself", id), nil)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s.billomat.net/api/v2/clients/myself", id), http.NoBody)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -98,6 +113,23 @@ func verifyBillomat(ctx context.Context, client *http.Client, id, key string) (b
|
||||
case http.StatusOK:
|
||||
return true, nil
|
||||
case http.StatusUnauthorized:
|
||||
return false, nil
|
||||
case http.StatusNotFound: // billomat api returns 404 if account id does not exist
|
||||
// read the full response body
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
/*
|
||||
The regex for capturing a Billomat ID is prone to false positives.
|
||||
To minimize incorrect matches, we return an error if the captured account ID does not exist,
|
||||
as this likely indicates the match was invalid.
|
||||
*/
|
||||
if strings.Contains(string(bodyBytes), "account not found") {
|
||||
return false, errAccountIDNotFound
|
||||
}
|
||||
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
|
||||
@@ -96,6 +96,7 @@ func TestBillomat_FromChunk(t *testing.T) {
|
||||
t.Fatalf("no raw secret present: \n %+v", got[i])
|
||||
}
|
||||
got[i].Raw = nil
|
||||
got[i].RawV2 = nil
|
||||
}
|
||||
if diff := pretty.Compare(got, tt.want); diff != "" {
|
||||
t.Errorf("Billomat.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
|
||||
@@ -10,37 +10,6 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
|
||||
)
|
||||
|
||||
var (
|
||||
validPattern = "billomatKey: xv3khh5klgzztdmptrgbqhkr0ucvr67i / billomatID: s2mels7c75tnsbs7ldu0wmjofzmugkg7vb"
|
||||
complexPattern = `
|
||||
func main() {
|
||||
url := "https://api.billomat.net/v2/s2mels7c75tnsbs7ldu0wmjofzmugkg7vb"
|
||||
|
||||
// Create a new request with the secret as a header
|
||||
req, err := http.NewRequest("GET", url, http.NoBody)
|
||||
if err != nil {
|
||||
fmt.Println("Error creating request:", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("X-BillomatApiKey", "xv3khh5klgzztdmptrgbqhkr0ucvr67i")
|
||||
|
||||
// Perform the request
|
||||
client := &http.Client{}
|
||||
resp, _ := client.Do(req)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check response status
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
fmt.Println("Request successful!")
|
||||
} else {
|
||||
fmt.Println("Request failed with status:", resp.Status)
|
||||
}
|
||||
}
|
||||
`
|
||||
invalidPattern = "billomat_creds: s2mels7c75tnsbs7ldu0wmjofzmugkg7vb"
|
||||
)
|
||||
|
||||
func TestBilloMat_Pattern(t *testing.T) {
|
||||
d := Scanner{}
|
||||
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
|
||||
@@ -51,25 +20,43 @@ func TestBilloMat_Pattern(t *testing.T) {
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "valid pattern",
|
||||
input: validPattern,
|
||||
name: "valid pattern",
|
||||
input: `
|
||||
func main() {
|
||||
url := "https://api.billomat.net/v2/id/truffletest"
|
||||
|
||||
// Create a new request with the secret as a header
|
||||
req, err := http.NewRequest("GET", url, http.NoBody)
|
||||
if err != nil {
|
||||
fmt.Println("Error creating request:", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("X-BillomatApiKey", "c09761f99f39f79ae28eaaf8df20d7c9")
|
||||
|
||||
// Perform the request
|
||||
client := &http.Client{}
|
||||
resp, _ := client.Do(req)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check response status
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
fmt.Println("Request successful!")
|
||||
} else {
|
||||
fmt.Println("Request failed with status:", resp.Status)
|
||||
}
|
||||
}`,
|
||||
want: []string{
|
||||
"xv3khh5klgzztdmptrgbqhkr0ucvr67is2mels7c75tnsbs7ldu0wmjofzmugkg7vb",
|
||||
"xv3khh5klgzztdmptrgbqhkr0ucvr67ixv3khh5klgzztdmptrgbqhkr0ucvr67i",
|
||||
"c09761f99f39f79ae28eaaf8df20d7c9truffletest",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid pattern - complex",
|
||||
input: complexPattern,
|
||||
want: []string{
|
||||
"xv3khh5klgzztdmptrgbqhkr0ucvr67inet",
|
||||
"xv3khh5klgzztdmptrgbqhkr0ucvr67ixv3khh5klgzztdmptrgbqhkr0ucvr67i",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid pattern",
|
||||
input: invalidPattern,
|
||||
want: nil,
|
||||
name: "invalid pattern",
|
||||
input: `
|
||||
req.Header.Set("X-BillomatApiKey", "c09761h99f39f79ae28eaaf8df20d7c9")
|
||||
billomatID := truffle-test
|
||||
`,
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -7,15 +7,15 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v7"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
|
||||
)
|
||||
|
||||
var (
|
||||
clientId = gofakeit.Password(true, true, true, false, false, 32)
|
||||
clientSecret = gofakeit.Password(true, true, true, false, false, 32)
|
||||
invalidClientSecret = gofakeit.Password(true, true, true, true, false, 32)
|
||||
clientId = common.GenerateRandomPassword(true, true, true, false, 32)
|
||||
clientSecret = common.GenerateRandomPassword(true, true, true, false, 32)
|
||||
invalidClientSecret = common.GenerateRandomPassword(true, true, true, true, 32)
|
||||
)
|
||||
|
||||
func TestBoxOauth_Pattern(t *testing.T) {
|
||||
|
||||
@@ -2,74 +2,205 @@ package coinbase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type Scanner struct{}
|
||||
type Scanner struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// Ensure the Scanner satisfies the interface at compile time.
|
||||
var _ detectors.Detector = (*Scanner)(nil)
|
||||
|
||||
var (
|
||||
client = common.SaneHttpClient()
|
||||
defaultClient = common.SaneHttpClient()
|
||||
|
||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"coinbase"}) + `\b([a-zA-Z-0-9]{64})\b`)
|
||||
// Reference: https://docs.cdp.coinbase.com/coinbase-app/docs/auth/api-key-authentication
|
||||
keyNamePat = regexp.MustCompile(`\b(organizations\\*/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\\*/apiKeys\\*/\w{8}-\w{4}-\w{4}-\w{4}-\w{12})\b`)
|
||||
privateKeyPat = regexp.MustCompile(`(-----BEGIN EC(?:DSA)? PRIVATE KEY-----(?:\r|\n|\\+r|\\+n)(?:[a-zA-Z0-9+/]+={0,2}(?:\r|\n|\\+r|\\+n))+-----END EC(?:DSA)? PRIVATE KEY-----(?:\r|\n|\\+r|\\+n)?)`)
|
||||
|
||||
apiHost = "api.coinbase.com"
|
||||
verificationEndpoint = "/v2/user"
|
||||
verificationMethod = http.MethodGet
|
||||
verificationURI = fmt.Sprintf("https://%s%s", apiHost, verificationEndpoint)
|
||||
|
||||
nameReplacer = strings.NewReplacer("\\", "")
|
||||
keyReplacer = strings.NewReplacer(
|
||||
"\r\n", "\n",
|
||||
"\\r\\n", "\n",
|
||||
"\\n", "\n",
|
||||
"\\r", "\n",
|
||||
)
|
||||
)
|
||||
|
||||
// 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{"coinbase"}
|
||||
return []string{"begin ec"}
|
||||
}
|
||||
|
||||
func isValidECPrivateKey(pemKey []byte) bool {
|
||||
block, _ := pem.Decode(pemKey)
|
||||
if block == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
key, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check the key type
|
||||
_, ok := key.Public().(*ecdsa.PublicKey)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s Scanner) getClient() *http.Client {
|
||||
if s.client != nil {
|
||||
return s.client
|
||||
}
|
||||
return defaultClient
|
||||
}
|
||||
|
||||
// FromData will find and optionally verify Coinbase secrets in a given set of bytes.
|
||||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
||||
dataStr := string(data)
|
||||
|
||||
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
|
||||
uniqueKeyNames, uniquePrivateKeys := map[string]struct{}{}, map[string]struct{}{}
|
||||
|
||||
for _, match := range matches {
|
||||
resMatch := strings.TrimSpace(match[1])
|
||||
for _, keyNameMatch := range keyNamePat.FindAllStringSubmatch(dataStr, -1) {
|
||||
uniqueKeyNames[keyNameMatch[1]] = struct{}{}
|
||||
}
|
||||
|
||||
s1 := detectors.Result{
|
||||
DetectorType: detectorspb.DetectorType_Coinbase,
|
||||
Raw: []byte(resMatch),
|
||||
}
|
||||
for _, privateKeyMatch := range privateKeyPat.FindAllStringSubmatch(dataStr, -1) {
|
||||
uniquePrivateKeys[privateKeyMatch[1]] = struct{}{}
|
||||
}
|
||||
|
||||
if verify {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.coinbase.com/v2/user", nil)
|
||||
if err != nil {
|
||||
for keyName := range uniqueKeyNames {
|
||||
for privateKey := range uniquePrivateKeys {
|
||||
client := s.getClient()
|
||||
resKeyName := nameReplacer.Replace(strings.TrimSpace(keyName))
|
||||
resPrivateKey := keyReplacer.Replace(strings.TrimSpace(privateKey))
|
||||
|
||||
if !isValidECPrivateKey([]byte(resPrivateKey)) {
|
||||
continue
|
||||
}
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
|
||||
res, err := client.Do(req)
|
||||
if err == nil {
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||
s1.Verified = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, s1)
|
||||
s1 := detectors.Result{
|
||||
DetectorType: detectorspb.DetectorType_Coinbase,
|
||||
Raw: []byte(resPrivateKey),
|
||||
RawV2: []byte(fmt.Sprintf("%s:%s", resKeyName, resPrivateKey)),
|
||||
}
|
||||
|
||||
if verify {
|
||||
isVerified, verificationErr := s.verifyMatch(ctx, client, resKeyName, resPrivateKey)
|
||||
s1.Verified = isVerified
|
||||
s1.SetVerificationError(verificationErr, resPrivateKey)
|
||||
}
|
||||
results = append(results, s1)
|
||||
|
||||
// If we've found a verified match with this ID, we don't need to look for anymore. So move on to the next ID.
|
||||
if s1.Verified {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, keyName, privateKey string) (bool, error) {
|
||||
jwtToken, err := buildJWT(verificationMethod, apiHost, verificationEndpoint, keyName, privateKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, verificationMethod, verificationURI, http.NoBody)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", jwtToken))
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, res.Body)
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
|
||||
switch res.StatusCode {
|
||||
case http.StatusOK:
|
||||
return true, nil
|
||||
case http.StatusUnauthorized:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Coinbase API requires the credentials encoded in a JWT token
|
||||
// The JWT token is signed with the private key and expires in 2 minutes
|
||||
func buildJWT(method, host, endpoint, keyName, key string) (string, error) {
|
||||
// Decode the PEM key
|
||||
pemStr := strings.ReplaceAll(key, `\n`, "\n")
|
||||
block, _ := pem.Decode([]byte(pemStr))
|
||||
if block == nil || block.Type != "EC PRIVATE KEY" {
|
||||
return "", fmt.Errorf("failed to decode PEM block containing EC private key")
|
||||
}
|
||||
|
||||
privateKey, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse EC private key: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
claims := jwt.MapClaims{
|
||||
"sub": keyName,
|
||||
"iss": "cdp",
|
||||
"nbf": now,
|
||||
"exp": now + 120,
|
||||
"uri": fmt.Sprintf("%s %s%s", method, host, endpoint),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
|
||||
token.Header["kid"] = keyName
|
||||
token.Header["nonce"] = fmt.Sprintf("%x", makeNonce())
|
||||
|
||||
signedToken, err := token.SignedString(privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign JWT: %v", err)
|
||||
}
|
||||
|
||||
return signedToken, nil
|
||||
}
|
||||
|
||||
func makeNonce() []byte {
|
||||
nonce := make([]byte, 16) // 128-bit nonce
|
||||
_, _ = rand.Read(nonce)
|
||||
return nonce
|
||||
}
|
||||
|
||||
func (s Scanner) Type() detectorspb.DetectorType {
|
||||
return detectorspb.DetectorType_Coinbase
|
||||
}
|
||||
|
||||
func (s Scanner) Description() string {
|
||||
return "Coinbase is a digital currency exchange that allows users to buy, sell, and store various cryptocurrencies. A Coinbase API key can be used to access and manage a user's account and transactions."
|
||||
return "Coinbase is a digital currency exchange that allows users to buy, sell, and store various cryptocurrencies. A Coinbase API key name and private key can be used to access and manage a user's account and transactions."
|
||||
}
|
||||
|
||||
@@ -6,25 +6,31 @@ package coinbase
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"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"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
|
||||
)
|
||||
|
||||
func TestCoinbase_FromChunk(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
|
||||
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("COINBASE_TOKEN")
|
||||
inactiveSecret := testSecrets.MustGetField("COINBASE_INACTIVE")
|
||||
|
||||
keyName := testSecrets.MustGetField("COINBASE_KEY_NAME")
|
||||
privateKey := testSecrets.MustGetField("COINBASE_PRIVATE_KEY")
|
||||
inactiveKeyName := testSecrets.MustGetField("COINBASE_INACTIVE_KEY_NAME")
|
||||
inactivePrivateKey := testSecrets.MustGetField("COINBASE_INACTIVE_PRIVATE_KEY")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -32,18 +38,19 @@ func TestCoinbase_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 coinbase secret %s within", secret)),
|
||||
data: []byte(fmt.Sprintf("You can find a coinbase secret %s %s within", keyName, privateKey)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
@@ -52,14 +59,15 @@ func TestCoinbase_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 coinbase secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
|
||||
data: []byte(fmt.Sprintf("You can find a coinbase secret %s %s within but not valid", inactiveKeyName, inactivePrivateKey)), // the secret would satisfy the regex but not pass validation
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
@@ -68,7 +76,8 @@ func TestCoinbase_FromChunk(t *testing.T) {
|
||||
Verified: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantErr: false,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
@@ -78,25 +87,62 @@ func TestCoinbase_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 coinbase secret %s %s within", keyName, privateKey)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_Coinbase,
|
||||
Verified: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
{
|
||||
name: "found, verified but unexpected api surface",
|
||||
s: Scanner{client: common.ConstantResponseHttpClient(http.StatusInternalServerError, "")},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a coinbase secret %s %s within", keyName, privateKey)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_Coinbase,
|
||||
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("Coinbase.FromData() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
if len(got[i].Raw) == 0 {
|
||||
t.Fatal("no raw secret present")
|
||||
t.Fatalf("no raw secret present: \n %+v", got[i])
|
||||
}
|
||||
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
|
||||
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
|
||||
}
|
||||
got[i].Raw = nil
|
||||
}
|
||||
if diff := pretty.Compare(got, tt.want); diff != "" {
|
||||
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "primarySecret")
|
||||
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
|
||||
t.Errorf("Coinbase.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
}
|
||||
})
|
||||
@@ -108,8 +154,12 @@ func BenchmarkFromData(benchmark *testing.B) {
|
||||
s := Scanner{}
|
||||
for name, data := range detectors.MustGetBenchmarkData() {
|
||||
benchmark.Run(name, func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
s.FromData(ctx, false, data)
|
||||
_, err := s.FromData(ctx, false, data)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,88 +3,133 @@ package coinbase
|
||||
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 = `
|
||||
# Configuration File: config.yaml
|
||||
database:
|
||||
host: $DB_HOST
|
||||
port: $DB_PORT
|
||||
username: $DB_USERNAME
|
||||
password: $DB_PASS # IMPORTANT: Do not share this password publicly
|
||||
|
||||
api:
|
||||
auth_type: "Bearer"
|
||||
base_url: "https://api.example.com/v1/user"
|
||||
coinbase_key: "IIQle6bXgoQEzxqHBt2VN0gW-Yve3t5k5VVD3nNAUUMCLVdK-3M4k6apjs43l0nM"
|
||||
|
||||
# Notes:
|
||||
# - Remember to rotate the secret every 90 days.
|
||||
# - The above credentials should only be used in a secure environment.
|
||||
`
|
||||
secret = "IIQle6bXgoQEzxqHBt2VN0gW-Yve3t5k5VVD3nNAUUMCLVdK-3M4k6apjs43l0nM"
|
||||
)
|
||||
|
||||
func TestCoinBase_Pattern(t *testing.T) {
|
||||
d := Scanner{}
|
||||
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
|
||||
|
||||
func TestCoinbase_Pattern(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
name string
|
||||
data string
|
||||
shouldMatch bool
|
||||
match string
|
||||
}{
|
||||
// True positives
|
||||
// https://github.com/coinbase/waas-client-library-go/issues/41
|
||||
{
|
||||
name: "valid pattern",
|
||||
input: validPattern,
|
||||
want: []string{secret},
|
||||
name: "valid_result1",
|
||||
data: `{ "name": "organizations/14d1742b-3575-4490-b9bc-a8a9c7e4973d/apiKeys/7473d38c-80c6-4a69-a715-1ea8fd950f6f", "principal": "8feb538e-137b-5864-b12a-7c75b60fa20a", "principalType": "USER", "publicKey": "-----BEGIN EC PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzR0G+CW0uVJFrpLUELqB+DlsmGmO\nA03Az8Fpv7azpgjAy87ibgQTThaQy1C1BccbCDkPoEs6mOnDkOebkybAKQ==\n-----END EC PUBLIC KEY-----\n", "privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBddyynZ9Ya7op1B9nu1Dxyc1T6xLy72t45J2Smv9oXNoAoGCCqGSM49\nAwEHoUQDQgAEzR0G+CW0uVJFrpLUELqB+DlsmGmOA03Az8Fpv7azpgjAy87ibgQT\nThaQy1C1BccbCDkPoEs6mOnDkOebkybAKQ==\n-----END EC PRIVATE KEY-----\n", "createTime": "2023-08-19T12:29:08.938421763Z", "projectId": "5970e137-9c3d-4adc-b65d-58d33af2432d" }`,
|
||||
shouldMatch: true,
|
||||
match: "organizations/14d1742b-3575-4490-b9bc-a8a9c7e4973d/apiKeys/7473d38c-80c6-4a69-a715-1ea8fd950f6f:-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBddyynZ9Ya7op1B9nu1Dxyc1T6xLy72t45J2Smv9oXNoAoGCCqGSM49\nAwEHoUQDQgAEzR0G+CW0uVJFrpLUELqB+DlsmGmOA03Az8Fpv7azpgjAy87ibgQT\nThaQy1C1BccbCDkPoEs6mOnDkOebkybAKQ==\n-----END EC PRIVATE KEY-----\n",
|
||||
},
|
||||
// https://github.com/coinbase/waas-client-library-go/pull/32#issuecomment-1666415017
|
||||
{
|
||||
name: "valid_result2_name_slashes",
|
||||
data: `{
|
||||
"name": "organizations\/d3f266dc-0d36-4cd0-91c3-e3a292b0b4b3\/apiKeys\/032c4fdf-d763-4b0c-9ed3-ff41a873bcc8",
|
||||
"principal": "5d5c9f00-3224-52a7-a1f7-9e6ce3ada40c",
|
||||
"principalType": "USER",
|
||||
"publicKey": "-----BEGIN EC PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAjw43hwOqS2PF4gAFbhoxIJqCHAP\niqLdg5GFVn9QAS/0oY4/fJGrCn9rpQGOvHxHf1mtQ6j4bIWN1AtHvA/3uw==\n-----END EC PUBLIC KEY-----\n",
|
||||
"privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIFkA1kU4DlNu36wTTHycWy6n1rsUH0UT8mfAKNtOukXHoAoGCCqGSM49\nAwEHoUQDQgAEAjw43hwOqS2PF4gAFbhoxIJqCHAPiqLdg5GFVn9QAS/0oY4/fJGr\nCn9rpQGOvHxHf1mtQ6j4bIWN1AtHvA/3uw==\n-----END EC PRIVATE KEY-----\n",
|
||||
"createTime": "2023-08-05T06:34:40.265235553Z",
|
||||
"projectId": "64b3f391-c69d-4c59-91a2-75816c1a0738"
|
||||
}`,
|
||||
shouldMatch: true,
|
||||
match: "organizations/d3f266dc-0d36-4cd0-91c3-e3a292b0b4b3/apiKeys/032c4fdf-d763-4b0c-9ed3-ff41a873bcc8:-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIFkA1kU4DlNu36wTTHycWy6n1rsUH0UT8mfAKNtOukXHoAoGCCqGSM49\nAwEHoUQDQgAEAjw43hwOqS2PF4gAFbhoxIJqCHAPiqLdg5GFVn9QAS/0oY4/fJGr\nCn9rpQGOvHxHf1mtQ6j4bIWN1AtHvA/3uw==\n-----END EC PRIVATE KEY-----\n",
|
||||
},
|
||||
{
|
||||
name: "valid_result3",
|
||||
data: `name: "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9",
|
||||
description: "principal": "775fb863-004f-5412-8e4c-e9449c612563" and install dependencies
|
||||
|
||||
runs: "principalType": "USER",
|
||||
using: composite
|
||||
steps:"publicKey": "-----BEGIN EC PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvHsvI08kox+n/8wSMFwCbK5hEf5b\n/g82Lmz3HpATKFmrICcOBX2lRHo99JWRrupmjUGxnD8i4sj4mZafTEokhA==\n-----END EC PUBLIC KEY-----\n",
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with: "privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIKOQ7lvGL0EiUzZ23pmH/NBPRwVV8yZsqofds5bSR9qFoAoGCCqGSM49\nAwEHoUQDQgAEvHsvI08kox+n/8wSMFwCbK5hEf5b/g82Lmz3HpATKFmrICcOBX2l\nRHo99JWRrupmjUGxnD8i4sj4mZafTEokhA==\n-----END EC PRIVATE KEY-----\n",
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Cache dependencies`,
|
||||
shouldMatch: true,
|
||||
match: "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9:-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIKOQ7lvGL0EiUzZ23pmH/NBPRwVV8yZsqofds5bSR9qFoAoGCCqGSM49\nAwEHoUQDQgAEvHsvI08kox+n/8wSMFwCbK5hEf5b/g82Lmz3HpATKFmrICcOBX2l\nRHo99JWRrupmjUGxnD8i4sj4mZafTEokhA==\n-----END EC PRIVATE KEY-----\n",
|
||||
},
|
||||
{
|
||||
name: "valid_result_ecdsa",
|
||||
data: `{
|
||||
"name": "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9",
|
||||
"privateKey": "-----BEGIN ECDSA PRIVATE KEY-----\nMHcCAQEEINQdZMbF2r07KF0mxfLYt9Y1PNaC0C6UpZ31MxD4NEE8oAoGCCqGSM49\nAwEHoUQDQgAEeRFgMrQEHI/APWaziRH90jN7EozjdbPVxvzc1F4zqWTeCtLASwqA\nqnMugYX2epqsFhGn82xNXu2NwgORc6embQ==\n-----END ECDSA PRIVATE KEY-----\n"
|
||||
}`,
|
||||
shouldMatch: true,
|
||||
match: "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9:-----BEGIN ECDSA PRIVATE KEY-----\nMHcCAQEEINQdZMbF2r07KF0mxfLYt9Y1PNaC0C6UpZ31MxD4NEE8oAoGCCqGSM49\nAwEHoUQDQgAEeRFgMrQEHI/APWaziRH90jN7EozjdbPVxvzc1F4zqWTeCtLASwqA\nqnMugYX2epqsFhGn82xNXu2NwgORc6embQ==\n-----END ECDSA PRIVATE KEY-----\n",
|
||||
},
|
||||
// TODO: Is it worth supporting case-insensitive headers?
|
||||
// https://github.com/coinbase/waas-sdk-react-native/blob/bbaf597e73d02ecaf64161061e71b85d9eeeb9d6/example/src/.coinbase_cloud_api_key.json#L4
|
||||
// {
|
||||
// name: "valid_result_case_insensitive",
|
||||
// data: `{
|
||||
// "name": "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9",
|
||||
// "privateKey": "-----BEGIN ECDSA private key-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8id7yCfmNp0ppczu\nDhjB1pesdDB6Uwuz6KxARrenNfyhRANCAASI6DBntdr+XSOaK55J++x8ORuDxn81\nENa0RmGFjTwu4vQcWcx5rrIWNh6b7FPxy6mrZl0n3rswEtZmUci8Y5HX\n-----END ECDSA PRIVATE KEY-----\n"
|
||||
//}`,
|
||||
// shouldMatch: true,
|
||||
// match: "organizations/7eegad2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9:-----BEGIN ECDSA PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8id7yCfmNp0ppczu\nDhjB1pesdDB6Uwuz6KxARrenNfyhRANCAASI6DBntdr+XSOaK55J++x8ORuDxn81\nENa0RmGFjTwu4vQcWcx5rrIWNh6b7FPxy6mrZl0n3rswEtZmUci8Y5HX\n-----END ECDSA PRIVATE KEY-----\n",
|
||||
// },
|
||||
|
||||
// False positives
|
||||
// https://github.com/coinbase/waas-client-library-go/blob/main/example.go
|
||||
{
|
||||
name: `invalid_key_name1`,
|
||||
data: `const (
|
||||
// apiKeyName is the name of the API Key to use. Fill this out before running the main function.
|
||||
apiKeyName = "organizations/my-organization/apiKeys/my-api-key"
|
||||
|
||||
// privKeyTemplate is the private key of the API Key to use. Fill this out before running the main function.
|
||||
privKeyTemplate = "-----BEGIN EC PRIVATE KEY-----\nmy-private-key\n-----END EC PRIVATE KEY-----\n"
|
||||
)`,
|
||||
shouldMatch: false,
|
||||
},
|
||||
// https://github.com/coinbase/waas-sdk-react-native/blob/bbaf597e73d02ecaf64161061e71b85d9eeeb9d6/example/src/.coinbase_cloud_api_key.json#L4
|
||||
{
|
||||
name: `invalid_key_name2`,
|
||||
data: `{
|
||||
"name": "organizations/organizationID/apiKeys/apiKeyName",
|
||||
"privateKey": "-----BEGIN ECDSA Private Key-----ExamplePrivateKey-----END ECDSA Private Key-----\n"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: `invalid_private_key`,
|
||||
data: `{ "name": "organizations/14d1742b-3575-4490-b9bc-a8a9c7e4973d/apiKeys/7473d38c-80c6-4a69-a715-1ea8fd950f6f", "principal": "8feb538e-137b-5864-b12a-7c75b60fa20a", "principalType": "USER", "publicKey": "-----BEGIN EC PUBLIC KEY-----\ninvalid\n-----END EC PUBLIC KEY-----\n", "privateKey": "-----BEGIN EC PRIVATE KEY-----\ninvalid\n-----END EC PRIVATE KEY-----\n", "createTime": "2023-08-19T12:29:08.938421763Z", "projectId": "5970e137-9c3d-4adc-b65d-58d33af2432d" }`,
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
s := Scanner{}
|
||||
|
||||
results, err := d.FromData(context.Background(), false, []byte(test.input))
|
||||
results, err := s.FromData(context.Background(), false, []byte(test.data))
|
||||
if err != nil {
|
||||
t.Errorf("error = %v", err)
|
||||
t.Errorf("Coinbase.FromData() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(results) != len(test.want) {
|
||||
if test.shouldMatch {
|
||||
if len(results) == 0 {
|
||||
t.Errorf("did not receive result")
|
||||
} else {
|
||||
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
|
||||
t.Errorf("%s: did not receive a match for '%v' when one was expected", test.name, test.data)
|
||||
return
|
||||
}
|
||||
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 := test.data
|
||||
if test.match != "" {
|
||||
expected = test.match
|
||||
}
|
||||
result := results[0]
|
||||
resultData := string(result.RawV2)
|
||||
if resultData != expected {
|
||||
t.Errorf("%s: did not receive expected match.\n\texpected: '%s'\n\t actual: '%s'", test.name, expected, resultData)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if len(results) > 0 {
|
||||
t.Errorf("%s: received a match for '%v' when one wasn't wanted", test.name, test.data)
|
||||
return
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
package coinbase_waas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
|
||||
"github.com/coinbase/waas-client-library-go/auth"
|
||||
"github.com/coinbase/waas-client-library-go/clients"
|
||||
v1clients "github.com/coinbase/waas-client-library-go/clients/v1"
|
||||
v1 "github.com/coinbase/waas-client-library-go/gen/go/coinbase/cloud/pools/v1"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/api/googleapi"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
|
||||
)
|
||||
|
||||
type Scanner struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// Ensure the Scanner satisfies the interface at compile time.
|
||||
var _ detectors.Detector = (*Scanner)(nil)
|
||||
|
||||
var (
|
||||
// Reference: https://docs.cloud.coinbase.com/waas/docs/auth
|
||||
keyNamePat = regexp.MustCompile(`(organizations\\*/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\\*/apiKeys\\*/\w{8}-\w{4}-\w{4}-\w{4}-\w{12})`)
|
||||
privKeyPat = regexp.MustCompile(`(-----BEGIN EC(?:DSA)? PRIVATE KEY-----(?:\r|\n|\\+r|\\+n)(?:[a-zA-Z0-9+/]+={0,2}(?:\r|\n|\\+r|\\+n))+-----END EC(?:DSA)? PRIVATE KEY-----(?:\r|\n|\\+r|\\+n)?)`)
|
||||
|
||||
nameReplacer = strings.NewReplacer("\\", "")
|
||||
keyReplacer = strings.NewReplacer(
|
||||
"\r\n", "\n",
|
||||
"\\r\\n", "\n",
|
||||
"\\n", "\n",
|
||||
"\\r", "\n",
|
||||
)
|
||||
)
|
||||
|
||||
// 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{"organizations", "apiKeys", "begin ec"}
|
||||
}
|
||||
|
||||
const maxPrivateKeySize = 4096
|
||||
|
||||
// MaxSecretSize returns the maximum size of a secret that this detector can find.
|
||||
func (s Scanner) MaxSecretSize() int64 { return maxPrivateKeySize }
|
||||
|
||||
// FromData will find and optionally verify CoinbaseWaaS secrets in a given set of bytes.
|
||||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
||||
dataStr := string(data)
|
||||
|
||||
keyNameMatches := keyNamePat.FindAllStringSubmatch(dataStr, -1)
|
||||
privKeyMatches := privKeyPat.FindAllStringSubmatch(dataStr, -1)
|
||||
|
||||
for _, keyNameMatch := range keyNameMatches {
|
||||
resKeyNameMatch := nameReplacer.Replace(strings.TrimSpace(keyNameMatch[1]))
|
||||
|
||||
for _, privKeyMatch := range privKeyMatches {
|
||||
resPrivKeyMatch := keyReplacer.Replace(strings.TrimSpace(privKeyMatch[1]))
|
||||
|
||||
if !isValidECPrivateKey([]byte(resPrivKeyMatch)) {
|
||||
continue
|
||||
}
|
||||
|
||||
s1 := detectors.Result{
|
||||
DetectorType: detectorspb.DetectorType_CoinbaseWaaS,
|
||||
Raw: []byte(resPrivKeyMatch),
|
||||
RawV2: []byte(resKeyNameMatch + ":" + resPrivKeyMatch),
|
||||
}
|
||||
|
||||
if verify {
|
||||
isVerified, verificationErr := s.verifyMatch(ctx, resKeyNameMatch, resPrivKeyMatch)
|
||||
s1.Verified = isVerified
|
||||
s1.SetVerificationError(verificationErr, resPrivKeyMatch)
|
||||
}
|
||||
results = append(results, s1)
|
||||
|
||||
// If we've found a verified match with this ID, we don't need to look for anymore. So move on to the next ID.
|
||||
if s1.Verified {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func isValidECPrivateKey(pemKey []byte) bool {
|
||||
block, _ := pem.Decode(pemKey)
|
||||
if block == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
key, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check the key type
|
||||
if _, ok := key.Public().(*ecdsa.PublicKey); !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s Scanner) verifyMatch(ctx context.Context, apiKeyName, privKey string) (bool, error) {
|
||||
authOpt := clients.WithAPIKey(&auth.APIKey{
|
||||
Name: apiKeyName,
|
||||
PrivateKey: privKey,
|
||||
})
|
||||
clientOpt := clients.WithHTTPClient(s.client)
|
||||
client, err := v1clients.NewPoolServiceClient(ctx, authOpt, clientOpt)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Lookup an arbitrary pool name that shouldn't exist.
|
||||
_, err = client.GetPool(ctx, &v1.GetPoolRequest{Name: uuid.New().String()})
|
||||
if err != nil {
|
||||
var apiErr *googleapi.Error
|
||||
if errors.As(err, &apiErr) {
|
||||
if apiErr.Code == 401 {
|
||||
// Invalid |Name| or |PrivateKey|
|
||||
return false, nil
|
||||
} else if apiErr.Code == 404 {
|
||||
// Valid |Name| and |PrivateKey| but the pool doesn't exist (expected).
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
// Unhandled error.
|
||||
return false, err
|
||||
}
|
||||
// In theory this will never happen, but it also indicates a valid key.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s Scanner) Type() detectorspb.DetectorType {
|
||||
return detectorspb.DetectorType_CoinbaseWaaS
|
||||
}
|
||||
|
||||
func (s Scanner) Description() string {
|
||||
return "Coinbase WaaS (Wallet as a Service) provides a secure and scalable infrastructure for managing cryptocurrency wallets. The API keys allow access to this service to perform operations such as creating and managing wallets."
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
package coinbase_waas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCoinbaseWaaS_Pattern(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data string
|
||||
shouldMatch bool
|
||||
match string
|
||||
}{
|
||||
// True positives
|
||||
// https://github.com/coinbase/waas-client-library-go/issues/41
|
||||
{
|
||||
name: "valid_result1",
|
||||
data: `{ "name": "organizations/14d1742b-3575-4490-b9bc-a8a9c7e4973d/apiKeys/7473d38c-80c6-4a69-a715-1ea8fd950f6f", "principal": "8feb538e-137b-5864-b12a-7c75b60fa20a", "principalType": "USER", "publicKey": "-----BEGIN EC PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzR0G+CW0uVJFrpLUELqB+DlsmGmO\nA03Az8Fpv7azpgjAy87ibgQTThaQy1C1BccbCDkPoEs6mOnDkOebkybAKQ==\n-----END EC PUBLIC KEY-----\n", "privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBddyynZ9Ya7op1B9nu1Dxyc1T6xLy72t45J2Smv9oXNoAoGCCqGSM49\nAwEHoUQDQgAEzR0G+CW0uVJFrpLUELqB+DlsmGmOA03Az8Fpv7azpgjAy87ibgQT\nThaQy1C1BccbCDkPoEs6mOnDkOebkybAKQ==\n-----END EC PRIVATE KEY-----\n", "createTime": "2023-08-19T12:29:08.938421763Z", "projectId": "5970e137-9c3d-4adc-b65d-58d33af2432d" }`,
|
||||
shouldMatch: true,
|
||||
match: "organizations/14d1742b-3575-4490-b9bc-a8a9c7e4973d/apiKeys/7473d38c-80c6-4a69-a715-1ea8fd950f6f:-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIBddyynZ9Ya7op1B9nu1Dxyc1T6xLy72t45J2Smv9oXNoAoGCCqGSM49\nAwEHoUQDQgAEzR0G+CW0uVJFrpLUELqB+DlsmGmOA03Az8Fpv7azpgjAy87ibgQT\nThaQy1C1BccbCDkPoEs6mOnDkOebkybAKQ==\n-----END EC PRIVATE KEY-----\n",
|
||||
},
|
||||
// https://github.com/coinbase/waas-client-library-go/pull/32#issuecomment-1666415017
|
||||
{
|
||||
name: "valid_result2_name_slashes",
|
||||
data: `{
|
||||
"name": "organizations\/d3f266dc-0d36-4cd0-91c3-e3a292b0b4b3\/apiKeys\/032c4fdf-d763-4b0c-9ed3-ff41a873bcc8",
|
||||
"principal": "5d5c9f00-3224-52a7-a1f7-9e6ce3ada40c",
|
||||
"principalType": "USER",
|
||||
"publicKey": "-----BEGIN EC PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAjw43hwOqS2PF4gAFbhoxIJqCHAP\niqLdg5GFVn9QAS/0oY4/fJGrCn9rpQGOvHxHf1mtQ6j4bIWN1AtHvA/3uw==\n-----END EC PUBLIC KEY-----\n",
|
||||
"privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIFkA1kU4DlNu36wTTHycWy6n1rsUH0UT8mfAKNtOukXHoAoGCCqGSM49\nAwEHoUQDQgAEAjw43hwOqS2PF4gAFbhoxIJqCHAPiqLdg5GFVn9QAS/0oY4/fJGr\nCn9rpQGOvHxHf1mtQ6j4bIWN1AtHvA/3uw==\n-----END EC PRIVATE KEY-----\n",
|
||||
"createTime": "2023-08-05T06:34:40.265235553Z",
|
||||
"projectId": "64b3f391-c69d-4c59-91a2-75816c1a0738"
|
||||
}`,
|
||||
shouldMatch: true,
|
||||
match: "organizations/d3f266dc-0d36-4cd0-91c3-e3a292b0b4b3/apiKeys/032c4fdf-d763-4b0c-9ed3-ff41a873bcc8:-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIFkA1kU4DlNu36wTTHycWy6n1rsUH0UT8mfAKNtOukXHoAoGCCqGSM49\nAwEHoUQDQgAEAjw43hwOqS2PF4gAFbhoxIJqCHAPiqLdg5GFVn9QAS/0oY4/fJGr\nCn9rpQGOvHxHf1mtQ6j4bIWN1AtHvA/3uw==\n-----END EC PRIVATE KEY-----\n",
|
||||
},
|
||||
{
|
||||
name: "valid_result3",
|
||||
data: `name: "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9",
|
||||
description: "principal": "775fb863-004f-5412-8e4c-e9449c612563" and install dependencies
|
||||
|
||||
runs: "principalType": "USER",
|
||||
using: composite
|
||||
steps:"publicKey": "-----BEGIN EC PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvHsvI08kox+n/8wSMFwCbK5hEf5b\n/g82Lmz3HpATKFmrICcOBX2lRHo99JWRrupmjUGxnD8i4sj4mZafTEokhA==\n-----END EC PUBLIC KEY-----\n",
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with: "privateKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIKOQ7lvGL0EiUzZ23pmH/NBPRwVV8yZsqofds5bSR9qFoAoGCCqGSM49\nAwEHoUQDQgAEvHsvI08kox+n/8wSMFwCbK5hEf5b/g82Lmz3HpATKFmrICcOBX2l\nRHo99JWRrupmjUGxnD8i4sj4mZafTEokhA==\n-----END EC PRIVATE KEY-----\n",
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Cache dependencies`,
|
||||
shouldMatch: true,
|
||||
match: "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9:-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIKOQ7lvGL0EiUzZ23pmH/NBPRwVV8yZsqofds5bSR9qFoAoGCCqGSM49\nAwEHoUQDQgAEvHsvI08kox+n/8wSMFwCbK5hEf5b/g82Lmz3HpATKFmrICcOBX2l\nRHo99JWRrupmjUGxnD8i4sj4mZafTEokhA==\n-----END EC PRIVATE KEY-----\n",
|
||||
},
|
||||
{
|
||||
name: "valid_result_ecdsa",
|
||||
data: `{
|
||||
"name": "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9",
|
||||
"privateKey": "-----BEGIN ECDSA PRIVATE KEY-----\nMHcCAQEEINQdZMbF2r07KF0mxfLYt9Y1PNaC0C6UpZ31MxD4NEE8oAoGCCqGSM49\nAwEHoUQDQgAEeRFgMrQEHI/APWaziRH90jN7EozjdbPVxvzc1F4zqWTeCtLASwqA\nqnMugYX2epqsFhGn82xNXu2NwgORc6embQ==\n-----END ECDSA PRIVATE KEY-----\n"
|
||||
}`,
|
||||
shouldMatch: true,
|
||||
match: "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9:-----BEGIN ECDSA PRIVATE KEY-----\nMHcCAQEEINQdZMbF2r07KF0mxfLYt9Y1PNaC0C6UpZ31MxD4NEE8oAoGCCqGSM49\nAwEHoUQDQgAEeRFgMrQEHI/APWaziRH90jN7EozjdbPVxvzc1F4zqWTeCtLASwqA\nqnMugYX2epqsFhGn82xNXu2NwgORc6embQ==\n-----END ECDSA PRIVATE KEY-----\n",
|
||||
},
|
||||
// TODO: Is it worth supporting case-insensitive headers?
|
||||
// https://github.com/coinbase/waas-sdk-react-native/blob/bbaf597e73d02ecaf64161061e71b85d9eeeb9d6/example/src/.coinbase_cloud_api_key.json#L4
|
||||
// {
|
||||
// name: "valid_result_case_insensitive",
|
||||
// data: `{
|
||||
// "name": "organizations/7eead2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9",
|
||||
// "privateKey": "-----BEGIN ECDSA private key-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8id7yCfmNp0ppczu\nDhjB1pesdDB6Uwuz6KxARrenNfyhRANCAASI6DBntdr+XSOaK55J++x8ORuDxn81\nENa0RmGFjTwu4vQcWcx5rrIWNh6b7FPxy6mrZl0n3rswEtZmUci8Y5HX\n-----END ECDSA PRIVATE KEY-----\n"
|
||||
//}`,
|
||||
// shouldMatch: true,
|
||||
// match: "organizations/7eegad2d5-fa48-4423-8f40-c70d8ce398ae/apiKeys/7b9516b6-d82e-44e8-bed5-89b160452ed9:-----BEGIN ECDSA PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8id7yCfmNp0ppczu\nDhjB1pesdDB6Uwuz6KxARrenNfyhRANCAASI6DBntdr+XSOaK55J++x8ORuDxn81\nENa0RmGFjTwu4vQcWcx5rrIWNh6b7FPxy6mrZl0n3rswEtZmUci8Y5HX\n-----END ECDSA PRIVATE KEY-----\n",
|
||||
// },
|
||||
|
||||
// False positives
|
||||
// https://github.com/coinbase/waas-client-library-go/blob/main/example.go
|
||||
{
|
||||
name: `invalid_key_name1`,
|
||||
data: `const (
|
||||
// apiKeyName is the name of the API Key to use. Fill this out before running the main function.
|
||||
apiKeyName = "organizations/my-organization/apiKeys/my-api-key"
|
||||
|
||||
// privKeyTemplate is the private key of the API Key to use. Fill this out before running the main function.
|
||||
privKeyTemplate = "-----BEGIN EC PRIVATE KEY-----\nmy-private-key\n-----END EC PRIVATE KEY-----\n"
|
||||
)`,
|
||||
shouldMatch: false,
|
||||
},
|
||||
// https://github.com/coinbase/waas-sdk-react-native/blob/bbaf597e73d02ecaf64161061e71b85d9eeeb9d6/example/src/.coinbase_cloud_api_key.json#L4
|
||||
{
|
||||
name: `invalid_key_name2`,
|
||||
data: `{
|
||||
"name": "organizations/organizationID/apiKeys/apiKeyName",
|
||||
"privateKey": "-----BEGIN ECDSA Private Key-----ExamplePrivateKey-----END ECDSA Private Key-----\n"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: `invalid_private_key`,
|
||||
data: `{ "name": "organizations/14d1742b-3575-4490-b9bc-a8a9c7e4973d/apiKeys/7473d38c-80c6-4a69-a715-1ea8fd950f6f", "principal": "8feb538e-137b-5864-b12a-7c75b60fa20a", "principalType": "USER", "publicKey": "-----BEGIN EC PUBLIC KEY-----\ninvalid\n-----END EC PUBLIC KEY-----\n", "privateKey": "-----BEGIN EC PRIVATE KEY-----\ninvalid\n-----END EC PRIVATE KEY-----\n", "createTime": "2023-08-19T12:29:08.938421763Z", "projectId": "5970e137-9c3d-4adc-b65d-58d33af2432d" }`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
s := Scanner{}
|
||||
|
||||
results, err := s.FromData(context.Background(), false, []byte(test.data))
|
||||
if err != nil {
|
||||
t.Errorf("CoinbaseWaaS.FromData() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if test.shouldMatch {
|
||||
if len(results) == 0 {
|
||||
t.Errorf("%s: did not receive a match for '%v' when one was expected", test.name, test.data)
|
||||
return
|
||||
}
|
||||
expected := test.data
|
||||
if test.match != "" {
|
||||
expected = test.match
|
||||
}
|
||||
result := results[0]
|
||||
resultData := string(result.RawV2)
|
||||
if resultData != expected {
|
||||
t.Errorf("%s: did not receive expected match.\n\texpected: '%s'\n\t actual: '%s'", test.name, expected, resultData)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if len(results) > 0 {
|
||||
t.Errorf("%s: received a match for '%v' when one wasn't wanted", test.name, test.data)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package hasura
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Ensure the Scanner satisfies the interface at compile time.
|
||||
var _ detectors.Detector = (*Scanner)(nil)
|
||||
|
||||
var (
|
||||
defaultClient = common.SaneHttpClient()
|
||||
|
||||
// domainPat finds Hasura cloud domains.
|
||||
domainPat = regexp.MustCompile(`\b([a-zA-Z0-9-]+\.hasura\.app)\b`)
|
||||
// keyPat finds potential Hasura admin secrets, often prefixed with "hasura".
|
||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"hasura"}) + `\b([a-zA-Z0-9]{64})\b`)
|
||||
)
|
||||
|
||||
func (s Scanner) Keywords() []string {
|
||||
return []string{"hasura"}
|
||||
}
|
||||
|
||||
func (s Scanner) getClient() *http.Client {
|
||||
if s.client != nil {
|
||||
return s.client
|
||||
}
|
||||
|
||||
return defaultClient
|
||||
}
|
||||
|
||||
// FromData will find and optionally verify Hasura secrets in a given set of bytes.
|
||||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
||||
dataStr := string(data)
|
||||
|
||||
var uniqueKeyMatches, uniqueDomainMatches = make(map[string]struct{}), make(map[string]struct{})
|
||||
|
||||
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
|
||||
uniqueKeyMatches[match[1]] = struct{}{}
|
||||
}
|
||||
|
||||
for _, match := range domainPat.FindAllStringSubmatch(dataStr, -1) {
|
||||
uniqueDomainMatches[match[1]] = struct{}{}
|
||||
}
|
||||
|
||||
// Logic: For each key, try to verify against each found domain.
|
||||
// Stop and record a verified finding on the first successful match.
|
||||
for key := range uniqueKeyMatches {
|
||||
for domain := range uniqueDomainMatches {
|
||||
s1 := detectors.Result{
|
||||
DetectorType: detectorspb.DetectorType_Hasura,
|
||||
Raw: []byte(key),
|
||||
RawV2: fmt.Appendf([]byte(""), "%s:%s", domain, key),
|
||||
}
|
||||
|
||||
if verify {
|
||||
isVerified, extraData, verificationErr := s.verifyHasura(ctx, s.getClient(), domain, key)
|
||||
s1.Verified = isVerified
|
||||
s1.ExtraData = extraData
|
||||
s1.SetVerificationError(verificationErr, key)
|
||||
}
|
||||
|
||||
results = append(results, s1)
|
||||
// If we successfully verified this key with a domain, we don't need to check it against other domains.
|
||||
if s1.Verified {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s Scanner) Type() detectorspb.DetectorType {
|
||||
return detectorspb.DetectorType_Hasura
|
||||
}
|
||||
|
||||
func (s Scanner) Description() string {
|
||||
return `Hasura is an open-source engine that instantly generates GraphQL and REST APIs over PostgreSQL (and other databases).
|
||||
It allows you to query, mutate, and subscribe to data in real time. Admin secrets (or admin keys) are used to securely access
|
||||
and manage Hasura projects, giving full control over data, metadata, and schema.`
|
||||
}
|
||||
|
||||
// verifyHasura attempts to validate a Hasura key against a given domain.
|
||||
func (s Scanner) verifyHasura(ctx context.Context, client *http.Client, domain, key string) (bool, map[string]string, error) {
|
||||
query := `{"query":"query { __schema { types { name } } }"}`
|
||||
url := fmt.Sprintf("https://%s/v1/graphql", domain)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(query))
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("x-hasura-admin-secret", key)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
extraData := map[string]string{"domain": domain}
|
||||
|
||||
// Since the API returns 200 OK for both valid and invalid keys, we MUST parse the body.
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, extraData, err
|
||||
}
|
||||
|
||||
// Handle non-200 status codes
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// Special case: Project not reachable is a definitive "not verified", not an error
|
||||
if resp.StatusCode == http.StatusInternalServerError && isHasuraProjectUnavailable(bodyBytes) {
|
||||
return false, extraData, nil
|
||||
}
|
||||
return false, extraData, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Data any `json:"data"`
|
||||
Errors []any `json:"errors"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(bodyBytes, &response); err != nil {
|
||||
return false, extraData, fmt.Errorf("failed to unmarshal json response: %w", err)
|
||||
}
|
||||
|
||||
// Key is verified if we have data and no errors
|
||||
if response.Data != nil && len(response.Errors) == 0 {
|
||||
return true, extraData, nil
|
||||
}
|
||||
|
||||
// Key is not verified if we have errors or no data
|
||||
return false, extraData, nil
|
||||
}
|
||||
|
||||
func isHasuraProjectUnavailable(bodyBytes []byte) bool {
|
||||
return bytes.Contains(bodyBytes, []byte("Project not reachable")) || bytes.Contains(bodyBytes, []byte("Unable to load this project"))
|
||||
}
|
||||
+33
-31
@@ -1,11 +1,10 @@
|
||||
//go:build detectors
|
||||
// +build detectors
|
||||
|
||||
package coinbase_waas
|
||||
package hasura
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -13,32 +12,22 @@ import (
|
||||
"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"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
|
||||
)
|
||||
|
||||
func TestCoinbaseWaaS_FromChunk(t *testing.T) {
|
||||
func TestHasura_FromData(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)
|
||||
}
|
||||
secretb64 := testSecrets.MustGetField("COINBASE_WAAS")
|
||||
secretB, err := base64.StdEncoding.DecodeString(secretb64)
|
||||
if err != nil {
|
||||
t.Fatalf("could not decode secret: %s", err)
|
||||
}
|
||||
secret := string(secretB)
|
||||
|
||||
inactiveSecretb64 := testSecrets.MustGetField("COINBASE_WAAS_INACTIVE")
|
||||
inactiveSecretB, err := base64.StdEncoding.DecodeString(inactiveSecretb64)
|
||||
if err != nil {
|
||||
t.Fatalf("could not decode secret: %s", err)
|
||||
}
|
||||
inactiveSecret := string(inactiveSecretB)
|
||||
secret := testSecrets.MustGetField("HASURA")
|
||||
inactiveSecret := testSecrets.MustGetField("HASURA_INACTIVE")
|
||||
domain := testSecrets.MustGetField("HASURA_DOMAIN")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -58,13 +47,16 @@ func TestCoinbaseWaaS_FromChunk(t *testing.T) {
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a coinbase_waas secret %s within", secret)),
|
||||
data: []byte(fmt.Sprintf("You can find a hasura secret %s within hasura domain %s", secret, domain)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_CoinbaseWaaS,
|
||||
DetectorType: detectorspb.DetectorType_Hasura,
|
||||
Verified: true,
|
||||
Raw: []byte(secret),
|
||||
RawV2: []byte(fmt.Sprintf("%s:%s", domain, secret)),
|
||||
ExtraData: map[string]string{"domain": domain},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
@@ -75,13 +67,16 @@ func TestCoinbaseWaaS_FromChunk(t *testing.T) {
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a coinbase_waas secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
|
||||
data: []byte(fmt.Sprintf("You can find a hasura secret %s within hasura domain %s but not valid", inactiveSecret, domain)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_CoinbaseWaaS,
|
||||
DetectorType: detectorspb.DetectorType_Hasura,
|
||||
Verified: false,
|
||||
Raw: []byte(inactiveSecret),
|
||||
RawV2: []byte(fmt.Sprintf("%s:%s", domain, inactiveSecret)),
|
||||
ExtraData: map[string]string{"domain": domain},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
@@ -104,30 +99,36 @@ func TestCoinbaseWaaS_FromChunk(t *testing.T) {
|
||||
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a coinbase_waas secret %s within", secret)),
|
||||
data: []byte(fmt.Sprintf("You can find a hasura secret %s within %s", secret, domain)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_CoinbaseWaaS,
|
||||
DetectorType: detectorspb.DetectorType_Hasura,
|
||||
Verified: false,
|
||||
Raw: []byte(secret),
|
||||
RawV2: []byte(fmt.Sprintf("%s:%s", domain, secret)),
|
||||
ExtraData: nil,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
{
|
||||
name: "found, verified but unexpected api surface",
|
||||
name: "found, unexpected api response",
|
||||
s: Scanner{client: common.ConstantResponseHttpClient(500, "")},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a coinbase_waas secret %s within", secret)),
|
||||
data: []byte(fmt.Sprintf("You can find a hasura secret %s within %s", secret, domain)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_CoinbaseWaaS,
|
||||
DetectorType: detectorspb.DetectorType_Hasura,
|
||||
Verified: false,
|
||||
Raw: []byte(secret),
|
||||
RawV2: []byte(fmt.Sprintf("%s:%s", domain, secret)),
|
||||
ExtraData: map[string]string{"domain": domain},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
@@ -138,20 +139,21 @@ func TestCoinbaseWaaS_FromChunk(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Coinbasewaas.FromData() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("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])
|
||||
}
|
||||
|
||||
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
|
||||
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
|
||||
t.Fatalf("wantVerificationErr = %v, got verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
|
||||
}
|
||||
}
|
||||
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError")
|
||||
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
|
||||
t.Errorf("Coinbasewaas.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "verificationError", "primarySecret")
|
||||
if diff := cmp.Diff(tt.want, got, ignoreOpts); diff != "" {
|
||||
t.Errorf("Hasura.FromData() %s - diff: (-got +want)\n%s", tt.name, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package hasura
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestHasura_Pattern(t *testing.T) {
|
||||
d := Scanner{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedPairs []string
|
||||
}{
|
||||
{
|
||||
name: "simple case: one domain, one key",
|
||||
input: "The hasura admin secret is 05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4 for the domain project-one-12345.hasura.app",
|
||||
expectedPairs: []string{"project-one-12345.hasura.app:05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4"},
|
||||
},
|
||||
{
|
||||
name: "multiple keys, single domain",
|
||||
input: `
|
||||
The domain is project-one-12345.hasura.app
|
||||
hasura key1: 05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4
|
||||
hasura key2: aBcDeFgHiJkLmNoPqRsTuVwXyZ123456aBcDeFgHiJkLmNoPqRsTuVwXyZ123456
|
||||
`,
|
||||
expectedPairs: []string{
|
||||
"project-one-12345.hasura.app:05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4",
|
||||
"project-one-12345.hasura.app:aBcDeFgHiJkLmNoPqRsTuVwXyZ123456aBcDeFgHiJkLmNoPqRsTuVwXyZ123456",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single key, multiple domains",
|
||||
input: `
|
||||
The hasura key is 05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4
|
||||
Domain 1: project-one-12345.hasura.app
|
||||
Domain 2: project-two-67890.hasura.app
|
||||
`,
|
||||
expectedPairs: []string{
|
||||
"project-one-12345.hasura.app:05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4",
|
||||
"project-two-67890.hasura.app:05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "many-to-many: multiple keys and domains",
|
||||
input: `
|
||||
Here is a hasura key: 05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4
|
||||
And another hasura key: aBcDeFgHiJkLmNoPqRsTuVwXyZ123456aBcDeFgHiJkLmNoPqRsTuVwXyZ123456
|
||||
And a hasura domain: project-one-12345.hasura.app
|
||||
And another domain: project-two-67890.hasura.app
|
||||
`,
|
||||
expectedPairs: []string{
|
||||
"project-one-12345.hasura.app:05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4",
|
||||
"project-one-12345.hasura.app:aBcDeFgHiJkLmNoPqRsTuVwXyZ123456aBcDeFgHiJkLmNoPqRsTuVwXyZ123456",
|
||||
"project-two-67890.hasura.app:05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4",
|
||||
"project-two-67890.hasura.app:aBcDeFgHiJkLmNoPqRsTuVwXyZ123456aBcDeFgHiJkLmNoPqRsTuVwXyZ123456",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "negative case: only a key, no domain",
|
||||
input: "A hasura secret without a domain: 05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4",
|
||||
expectedPairs: []string{},
|
||||
},
|
||||
{
|
||||
name: "negative case: only a domain, no key",
|
||||
input: "A hasura domain without a secret: project-one-12345.hasura.app",
|
||||
expectedPairs: []string{},
|
||||
},
|
||||
{
|
||||
name: "negative case: invalid key with valid domain",
|
||||
input: "An invalid hasura key not-a-valid-key-12345 with a valid domain project-one-12345.hasura.app",
|
||||
expectedPairs: []string{},
|
||||
},
|
||||
{
|
||||
name: "mixed valid and invalid keys with one domain",
|
||||
input: `
|
||||
The domain is project-one-12345.hasura.app
|
||||
Valid hasura key: 05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4
|
||||
Invalid hasura key: not-a-valid-key-12345
|
||||
`,
|
||||
expectedPairs: []string{"project-one-12345.hasura.app:05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4"},
|
||||
},
|
||||
{
|
||||
name: "negative case: invalid domain with valid key",
|
||||
input: "A hasura key 05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4 with an invalid domain example.com",
|
||||
expectedPairs: []string{},
|
||||
},
|
||||
{
|
||||
name: "negative case: key without 'hasura' keyword nearby",
|
||||
input: "A random 64-char string 05tUFwoJfK2dui0CWKxzqJHmNzQrsX40Kwd7g2OEqLl1RZeU6pRvyOSnD4nghgH4 with a valid domain project-one-12345.hasura.app",
|
||||
expectedPairs: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Run the detector with verification turned off for pattern testing.
|
||||
results, err := d.FromData(context.Background(), false, []byte(test.input))
|
||||
if err != nil {
|
||||
t.Fatalf("FromData() returned an unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Check if the number of results matches what we expect.
|
||||
if len(results) != len(test.expectedPairs) {
|
||||
t.Errorf("expected %d results, but got %d", len(test.expectedPairs), len(results))
|
||||
// Log results for easier debugging
|
||||
for i, r := range results {
|
||||
t.Logf("Got result %d: %s", i, string(r.RawV2))
|
||||
}
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
// For a more robust comparison, load the results into maps to ignore order.
|
||||
actualPairs := make(map[string]struct{}, len(results))
|
||||
for _, r := range results {
|
||||
// The RawV2 field should contain the "domain:key" pair.
|
||||
if len(r.RawV2) > 0 {
|
||||
actualPairs[string(r.RawV2)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
expectedPairsMap := make(map[string]struct{}, len(test.expectedPairs))
|
||||
for _, v := range test.expectedPairs {
|
||||
expectedPairsMap[v] = struct{}{}
|
||||
}
|
||||
|
||||
// Use cmp.Diff to find any mismatches between the expected and actual pairs.
|
||||
if diff := cmp.Diff(expectedPairsMap, actualPairs); diff != "" {
|
||||
t.Errorf("FromData() results mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,66 +3,15 @@ package snowflake
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v7"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
|
||||
)
|
||||
|
||||
// GoFakeIt Password generator does not guarantee inclusion of characters.
|
||||
// Using a custom Password gennerator with guaranteed inclusions (atleast) of lower, upper and numeric characters
|
||||
func generatePassword(lower, upper, numeric bool, length int) string {
|
||||
if length < 1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var password []rune
|
||||
var required []rune
|
||||
var allowed []rune
|
||||
|
||||
lowerChars := []rune("abcdefghijklmnopqrstuvwxyz")
|
||||
upperChars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
numberChars := []rune("0123456789")
|
||||
|
||||
// Ensure inclusion from each requested category
|
||||
if lower {
|
||||
ch := lowerChars[rand.Intn(len(lowerChars))]
|
||||
required = append(required, ch)
|
||||
allowed = append(allowed, lowerChars...)
|
||||
}
|
||||
if upper {
|
||||
ch := upperChars[rand.Intn(len(upperChars))]
|
||||
required = append(required, ch)
|
||||
allowed = append(allowed, upperChars...)
|
||||
}
|
||||
if numeric {
|
||||
ch := numberChars[rand.Intn(len(numberChars))]
|
||||
required = append(required, ch)
|
||||
allowed = append(allowed, numberChars...)
|
||||
}
|
||||
|
||||
if len(allowed) == 0 {
|
||||
return "" // No character sets enabled
|
||||
}
|
||||
|
||||
// Fill the rest of the password
|
||||
for i := 0; i < length-len(required); i++ {
|
||||
ch := allowed[rand.Intn(len(allowed))]
|
||||
password = append(password, ch)
|
||||
}
|
||||
|
||||
// Combine required and random characters, then shuffle
|
||||
password = append(password, required...)
|
||||
rand.Shuffle(len(password), func(i, j int) {
|
||||
password[i], password[j] = password[j], password[i]
|
||||
})
|
||||
|
||||
return string(password)
|
||||
}
|
||||
|
||||
func TestSnowflake_Pattern(t *testing.T) {
|
||||
|
||||
validAccount := "tuacoip-zt74995"
|
||||
@@ -71,7 +20,7 @@ func TestSnowflake_Pattern(t *testing.T) {
|
||||
validUsername := gofakeit.Username()
|
||||
invalidUsername := "[email protected]" // special characters not allowed
|
||||
|
||||
validPassword := generatePassword(true, true, true, 10)
|
||||
validPassword := common.GenerateRandomPassword(true, true, true, false, 10)
|
||||
invalidPassword := "!12" // invalid length
|
||||
|
||||
d := Scanner{}
|
||||
|
||||
@@ -3,6 +3,7 @@ package sqlserver
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -57,12 +58,18 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
isVerified, err := ping(ctx, paramsUnsafe)
|
||||
|
||||
s1.Verified = isVerified
|
||||
|
||||
if mssqlErr, isMssqlErr := err.(mssql.Error); isMssqlErr && mssqlErr.Number == 18456 {
|
||||
// Login failed
|
||||
// Number taken from https://learn.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors?view=sql-server-ver16
|
||||
// Nothing to do; determinate failure to verify
|
||||
} else {
|
||||
mssqlErr, isMssqlErr := err.(mssql.Error)
|
||||
if isMssqlErr {
|
||||
if mssqlErr.Number == 18456 {
|
||||
// Login failed
|
||||
// Number taken from https://learn.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors?view=sql-server-ver16
|
||||
// Nothing to do; determinate failure to verify
|
||||
} else {
|
||||
// If it is a MSSQL error, format the error with error number and message
|
||||
s1.SetVerificationError(fmt.Errorf("SQL Server error %d: %s", mssqlErr.Number, mssqlErr.Message), paramsUnsafe.Password)
|
||||
}
|
||||
} else if err != nil {
|
||||
// If it is an error but not of MSSQL error type, just set error as verification error
|
||||
s1.SetVerificationError(err, paramsUnsafe.Password)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/appoptics"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/appsynergy"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/apptivo"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/artifactory"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/artsy"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/asanaoauth"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/asanapersonalaccesstoken"
|
||||
@@ -174,7 +175,6 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/codequiry"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/coinapi"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/coinbase"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/coinbase_waas"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/coinlayer"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/coinlib"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/collect2"
|
||||
@@ -348,6 +348,7 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/happyscribe"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/harness"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/harvest"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/hasura"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/hellosign"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/helpcrunch"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/helpscout"
|
||||
@@ -898,7 +899,7 @@ func buildDetectorList() []detectors.Detector {
|
||||
&appoptics.Scanner{},
|
||||
&appsynergy.Scanner{},
|
||||
&apptivo.Scanner{},
|
||||
// &artifactory.Scanner{},
|
||||
&artifactory.Scanner{},
|
||||
&artsy.Scanner{},
|
||||
&asanaoauth.Scanner{},
|
||||
&asanapersonalaccesstoken.Scanner{},
|
||||
@@ -1027,7 +1028,6 @@ func buildDetectorList() []detectors.Detector {
|
||||
&codequiry.Scanner{},
|
||||
&coinapi.Scanner{},
|
||||
&coinbase.Scanner{},
|
||||
&coinbase_waas.Scanner{},
|
||||
&coinlayer.Scanner{},
|
||||
&coinlib.Scanner{},
|
||||
&collect2.Scanner{},
|
||||
@@ -1481,6 +1481,7 @@ func buildDetectorList() []detectors.Detector {
|
||||
&rocketreach.Scanner{},
|
||||
// &rockset.Scanner{},
|
||||
&roninapp.Scanner{},
|
||||
&rootly.Scanner{},
|
||||
&route4me.Scanner{},
|
||||
&rownd.Scanner{},
|
||||
&rubygems.Scanner{},
|
||||
@@ -1717,7 +1718,7 @@ func buildDetectorList() []detectors.Detector {
|
||||
&zulipchat.Scanner{},
|
||||
&stripepaymentintent.Scanner{},
|
||||
&bitbucketapppassword.Scanner{},
|
||||
&rootly.Scanner{}
|
||||
&hasura.Scanner{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1228,7 +1228,7 @@ func TestEngineInitializesCloudProviderDetectors(t *testing.T) {
|
||||
for _, det := range e.detectors {
|
||||
if endpoints, ok := det.(interface{ Endpoints(...string) []string }); ok {
|
||||
id := config.GetDetectorID(det)
|
||||
if len(endpoints.Endpoints()) == 0 {
|
||||
if len(endpoints.Endpoints()) == 0 && det.Type() != detectorspb.DetectorType_ArtifactoryAccessToken { // artifactory does not have any cloud endpoint
|
||||
t.Fatalf("detector %q Endpoints() is empty", id.String())
|
||||
}
|
||||
count++
|
||||
|
||||
+157
-152
@@ -1015,46 +1015,47 @@ const (
|
||||
DetectorType_Moralis DetectorType = 909
|
||||
DetectorType_BscScan DetectorType = 910
|
||||
// Deprecated: Marked as deprecated in detectors.proto.
|
||||
DetectorType_CoinMarketCap DetectorType = 911
|
||||
DetectorType_Percy DetectorType = 912
|
||||
DetectorType_TinesWebhook DetectorType = 913
|
||||
DetectorType_Pulumi DetectorType = 914
|
||||
DetectorType_SupabaseToken DetectorType = 915
|
||||
DetectorType_NuGetApiKey DetectorType = 916
|
||||
DetectorType_Aiven DetectorType = 917
|
||||
DetectorType_Prefect DetectorType = 918
|
||||
DetectorType_Docusign DetectorType = 919
|
||||
DetectorType_Couchbase DetectorType = 920
|
||||
DetectorType_Dockerhub DetectorType = 921
|
||||
DetectorType_TrufflehogEnterprise DetectorType = 922
|
||||
DetectorType_EnvoyApiKey DetectorType = 923
|
||||
DetectorType_GitHubOauth2 DetectorType = 924
|
||||
DetectorType_Salesforce DetectorType = 925
|
||||
DetectorType_HuggingFace DetectorType = 926
|
||||
DetectorType_Snowflake DetectorType = 927
|
||||
DetectorType_Sourcegraph DetectorType = 928
|
||||
DetectorType_Tailscale DetectorType = 929
|
||||
DetectorType_Web3Storage DetectorType = 930
|
||||
DetectorType_AzureStorage DetectorType = 931
|
||||
DetectorType_PlanetScaleDb DetectorType = 932
|
||||
DetectorType_Anthropic DetectorType = 933
|
||||
DetectorType_Ramp DetectorType = 934
|
||||
DetectorType_Klaviyo DetectorType = 935
|
||||
DetectorType_SourcegraphCody DetectorType = 936
|
||||
DetectorType_Voiceflow DetectorType = 937
|
||||
DetectorType_Privacy DetectorType = 938
|
||||
DetectorType_IPInfo DetectorType = 939
|
||||
DetectorType_Ip2location DetectorType = 940
|
||||
DetectorType_Instamojo DetectorType = 941
|
||||
DetectorType_Portainer DetectorType = 942
|
||||
DetectorType_PortainerToken DetectorType = 943
|
||||
DetectorType_Loggly DetectorType = 944
|
||||
DetectorType_OpenVpn DetectorType = 945
|
||||
DetectorType_VagrantCloudPersonalToken DetectorType = 946
|
||||
DetectorType_BetterStack DetectorType = 947
|
||||
DetectorType_ZeroTier DetectorType = 948
|
||||
DetectorType_AppOptics DetectorType = 949
|
||||
DetectorType_Metabase DetectorType = 950
|
||||
DetectorType_CoinMarketCap DetectorType = 911
|
||||
DetectorType_Percy DetectorType = 912
|
||||
DetectorType_TinesWebhook DetectorType = 913
|
||||
DetectorType_Pulumi DetectorType = 914
|
||||
DetectorType_SupabaseToken DetectorType = 915
|
||||
DetectorType_NuGetApiKey DetectorType = 916
|
||||
DetectorType_Aiven DetectorType = 917
|
||||
DetectorType_Prefect DetectorType = 918
|
||||
DetectorType_Docusign DetectorType = 919
|
||||
DetectorType_Couchbase DetectorType = 920
|
||||
DetectorType_Dockerhub DetectorType = 921
|
||||
DetectorType_TrufflehogEnterprise DetectorType = 922
|
||||
DetectorType_EnvoyApiKey DetectorType = 923
|
||||
DetectorType_GitHubOauth2 DetectorType = 924
|
||||
DetectorType_Salesforce DetectorType = 925
|
||||
DetectorType_HuggingFace DetectorType = 926
|
||||
DetectorType_Snowflake DetectorType = 927
|
||||
DetectorType_Sourcegraph DetectorType = 928
|
||||
DetectorType_Tailscale DetectorType = 929
|
||||
DetectorType_Web3Storage DetectorType = 930
|
||||
DetectorType_AzureStorage DetectorType = 931
|
||||
DetectorType_PlanetScaleDb DetectorType = 932
|
||||
DetectorType_Anthropic DetectorType = 933
|
||||
DetectorType_Ramp DetectorType = 934
|
||||
DetectorType_Klaviyo DetectorType = 935
|
||||
DetectorType_SourcegraphCody DetectorType = 936
|
||||
DetectorType_Voiceflow DetectorType = 937
|
||||
DetectorType_Privacy DetectorType = 938
|
||||
DetectorType_IPInfo DetectorType = 939
|
||||
DetectorType_Ip2location DetectorType = 940
|
||||
DetectorType_Instamojo DetectorType = 941
|
||||
DetectorType_Portainer DetectorType = 942
|
||||
DetectorType_PortainerToken DetectorType = 943
|
||||
DetectorType_Loggly DetectorType = 944
|
||||
DetectorType_OpenVpn DetectorType = 945
|
||||
DetectorType_VagrantCloudPersonalToken DetectorType = 946
|
||||
DetectorType_BetterStack DetectorType = 947
|
||||
DetectorType_ZeroTier DetectorType = 948
|
||||
DetectorType_AppOptics DetectorType = 949
|
||||
DetectorType_Metabase DetectorType = 950
|
||||
// Deprecated: Marked as deprecated in detectors.proto.
|
||||
DetectorType_CoinbaseWaaS DetectorType = 951
|
||||
DetectorType_LemonSqueezy DetectorType = 952
|
||||
DetectorType_Budibase DetectorType = 953
|
||||
@@ -1134,7 +1135,8 @@ const (
|
||||
DetectorType_StripePaymentIntent DetectorType = 1027
|
||||
DetectorType_LangSmith DetectorType = 1028
|
||||
DetectorType_BitbucketAppPassword DetectorType = 1029
|
||||
DetectorType_Rootly DetectorType = 1030
|
||||
DetectorType_Hasura DetectorType = 1030
|
||||
DetectorType_Rootly DetectorType = 1031
|
||||
)
|
||||
|
||||
// Enum value maps for DetectorType.
|
||||
@@ -2166,7 +2168,8 @@ var (
|
||||
1027: "StripePaymentIntent",
|
||||
1028: "LangSmith",
|
||||
1029: "BitbucketAppPassword",
|
||||
1030: "Rootly",
|
||||
1030: "Hasura",
|
||||
1031: "Rootly",
|
||||
}
|
||||
DetectorType_value = map[string]int32{
|
||||
"Alibaba": 0,
|
||||
@@ -3195,7 +3198,8 @@ var (
|
||||
"StripePaymentIntent": 1027,
|
||||
"LangSmith": 1028,
|
||||
"BitbucketAppPassword": 1029,
|
||||
"Rootly": 1030,
|
||||
"Hasura": 1030,
|
||||
"Rootly": 1031,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3649,7 +3653,7 @@ var file_detectors_proto_rawDesc = []byte{
|
||||
0x4c, 0x41, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x41, 0x53, 0x45, 0x36, 0x34,
|
||||
0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x55, 0x54, 0x46, 0x31, 0x36, 0x10, 0x03, 0x12, 0x13, 0x0a,
|
||||
0x0f, 0x45, 0x53, 0x43, 0x41, 0x50, 0x45, 0x44, 0x5f, 0x55, 0x4e, 0x49, 0x43, 0x4f, 0x44, 0x45,
|
||||
0x10, 0x04, 0x2a, 0x81, 0x85, 0x01, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72,
|
||||
0x10, 0x04, 0x2a, 0x92, 0x85, 0x01, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72,
|
||||
0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x10,
|
||||
0x00, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x4d, 0x51, 0x50, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x41,
|
||||
0x57, 0x53, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x10, 0x03, 0x12,
|
||||
@@ -4610,114 +4614,115 @@ var file_detectors_proto_rawDesc = []byte{
|
||||
0x42, 0x65, 0x74, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x10, 0xb3, 0x07, 0x12, 0x0d,
|
||||
0x0a, 0x08, 0x5a, 0x65, 0x72, 0x6f, 0x54, 0x69, 0x65, 0x72, 0x10, 0xb4, 0x07, 0x12, 0x0e, 0x0a,
|
||||
0x09, 0x41, 0x70, 0x70, 0x4f, 0x70, 0x74, 0x69, 0x63, 0x73, 0x10, 0xb5, 0x07, 0x12, 0x0d, 0x0a,
|
||||
0x08, 0x4d, 0x65, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x10, 0xb6, 0x07, 0x12, 0x11, 0x0a, 0x0c,
|
||||
0x43, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x57, 0x61, 0x61, 0x53, 0x10, 0xb7, 0x07, 0x12,
|
||||
0x11, 0x0a, 0x0c, 0x4c, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x71, 0x75, 0x65, 0x65, 0x7a, 0x79, 0x10,
|
||||
0xb8, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x42, 0x75, 0x64, 0x69, 0x62, 0x61, 0x73, 0x65, 0x10, 0xb9,
|
||||
0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x44, 0x65, 0x6e, 0x6f, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x10,
|
||||
0xba, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x53, 0x74, 0x72, 0x69, 0x70, 0x6f, 0x10, 0xbb, 0x07, 0x12,
|
||||
0x0c, 0x0a, 0x07, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x49, 0x4f, 0x10, 0xbc, 0x07, 0x12, 0x0f, 0x0a,
|
||||
0x0a, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x10, 0xbd, 0x07, 0x12, 0x1b,
|
||||
0x0a, 0x16, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
|
||||
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x10, 0xbe, 0x07, 0x12, 0x12, 0x0a, 0x0d, 0x41,
|
||||
0x57, 0x53, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xbf, 0x07, 0x12,
|
||||
0x09, 0x0a, 0x04, 0x43, 0x6f, 0x64, 0x61, 0x10, 0xc0, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x4c, 0x6f,
|
||||
0x67, 0x7a, 0x49, 0x4f, 0x10, 0xc1, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x45, 0x76, 0x65, 0x6e, 0x74,
|
||||
0x62, 0x72, 0x69, 0x74, 0x65, 0x10, 0xc2, 0x07, 0x12, 0x1a, 0x0a, 0x15, 0x47, 0x72, 0x61, 0x66,
|
||||
0x61, 0x6e, 0x61, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e,
|
||||
0x74, 0x10, 0xc3, 0x07, 0x12, 0x13, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x46,
|
||||
0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x10, 0xc4, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x4f, 0x76, 0x65,
|
||||
0x72, 0x6c, 0x6f, 0x6f, 0x70, 0x10, 0xc5, 0x07, 0x12, 0x0a, 0x0a, 0x05, 0x4e, 0x67, 0x72, 0x6f,
|
||||
0x6b, 0x10, 0xc6, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74,
|
||||
0x65, 0x10, 0xc7, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x50, 0x6f, 0x73, 0x74, 0x67, 0x72, 0x65, 0x73,
|
||||
0x10, 0xc8, 0x07, 0x12, 0x2a, 0x0a, 0x25, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x41, 0x63, 0x74, 0x69,
|
||||
0x76, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x41, 0x70, 0x70, 0x6c, 0x69,
|
||||
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x10, 0xc9, 0x07, 0x12,
|
||||
0x20, 0x0a, 0x1b, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x46, 0x6f, 0x72,
|
||||
0x52, 0x65, 0x64, 0x69, 0x73, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x10, 0xca,
|
||||
0x07, 0x12, 0x21, 0x0a, 0x1c, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73,
|
||||
0x44, 0x42, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c,
|
||||
0x65, 0x10, 0xcb, 0x07, 0x12, 0x23, 0x0a, 0x1e, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x44, 0x65, 0x76,
|
||||
0x6f, 0x70, 0x73, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x65, 0x73,
|
||||
0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xcc, 0x07, 0x12, 0x15, 0x0a, 0x10, 0x41, 0x7a, 0x75,
|
||||
0x72, 0x65, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xcd, 0x07,
|
||||
0x12, 0x2c, 0x0a, 0x27, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4d, 0x4c, 0x57, 0x65, 0x62, 0x53, 0x65,
|
||||
0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x63, 0x49, 0x64, 0x65, 0x6e,
|
||||
0x74, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x10, 0xce, 0x07, 0x12, 0x12,
|
||||
0x0a, 0x0d, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x53, 0x61, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10,
|
||||
0xcf, 0x07, 0x12, 0x18, 0x0a, 0x13, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63,
|
||||
0x68, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xd0, 0x07, 0x12, 0x18, 0x0a, 0x13,
|
||||
0x41, 0x7a, 0x75, 0x72, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x51, 0x75, 0x65, 0x72, 0x79,
|
||||
0x4b, 0x65, 0x79, 0x10, 0xd1, 0x07, 0x12, 0x1f, 0x0a, 0x1a, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4d,
|
||||
0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69,
|
||||
0x63, 0x61, 0x74, 0x65, 0x10, 0xd2, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x41, 0x7a, 0x75, 0x72, 0x65,
|
||||
0x53, 0x51, 0x4c, 0x10, 0xd3, 0x07, 0x12, 0x0a, 0x0a, 0x05, 0x46, 0x6c, 0x79, 0x49, 0x4f, 0x10,
|
||||
0xd4, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x42, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x69, 0x74, 0x68, 0x10,
|
||||
0xd5, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x4a, 0x75, 0x70, 0x69, 0x74, 0x65, 0x72, 0x4f, 0x6e, 0x65,
|
||||
0x10, 0xd6, 0x07, 0x12, 0x25, 0x0a, 0x20, 0x47, 0x43, 0x50, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x72, 0x65, 0x64,
|
||||
0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x10, 0xd7, 0x07, 0x12, 0x08, 0x0a, 0x03, 0x57, 0x69,
|
||||
0x7a, 0x10, 0xd8, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x50, 0x61, 0x67, 0x61, 0x72, 0x6d, 0x65, 0x10,
|
||||
0xd9, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x4f, 0x6e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x10, 0xda, 0x07,
|
||||
0x12, 0x0c, 0x0a, 0x07, 0x49, 0x6e, 0x74, 0x72, 0x61, 0x34, 0x32, 0x10, 0xdb, 0x07, 0x12, 0x09,
|
||||
0x0a, 0x04, 0x47, 0x72, 0x6f, 0x71, 0x10, 0xdc, 0x07, 0x12, 0x17, 0x0a, 0x12, 0x54, 0x77, 0x69,
|
||||
0x74, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x72, 0x6b, 0x65, 0x79, 0x10,
|
||||
0xdd, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x45, 0x72, 0x61, 0x73, 0x65, 0x72, 0x10, 0xde, 0x07, 0x12,
|
||||
0x0e, 0x0a, 0x09, 0x4c, 0x61, 0x72, 0x6b, 0x53, 0x75, 0x69, 0x74, 0x65, 0x10, 0xdf, 0x07, 0x12,
|
||||
0x14, 0x0a, 0x0f, 0x4c, 0x61, 0x72, 0x6b, 0x53, 0x75, 0x69, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b,
|
||||
0x65, 0x79, 0x10, 0xe0, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x45, 0x6e, 0x64, 0x6f, 0x72, 0x4c, 0x61,
|
||||
0x62, 0x73, 0x10, 0xe1, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x45, 0x6c, 0x65, 0x76, 0x65, 0x6e, 0x4c,
|
||||
0x61, 0x62, 0x73, 0x10, 0xe2, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x4e, 0x65, 0x74, 0x73, 0x75, 0x69,
|
||||
0x74, 0x65, 0x10, 0xe3, 0x07, 0x12, 0x14, 0x0a, 0x0f, 0x52, 0x6f, 0x62, 0x69, 0x6e, 0x68, 0x6f,
|
||||
0x6f, 0x64, 0x43, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x10, 0xe4, 0x07, 0x12, 0x0a, 0x0a, 0x05, 0x4e,
|
||||
0x56, 0x41, 0x50, 0x49, 0x10, 0xe5, 0x07, 0x12, 0x09, 0x0a, 0x04, 0x50, 0x79, 0x50, 0x49, 0x10,
|
||||
0xe6, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x52, 0x61, 0x69, 0x6c, 0x77, 0x61, 0x79, 0x41, 0x70, 0x70,
|
||||
0x10, 0xe7, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x4d, 0x65, 0x72, 0x61, 0x6b, 0x69, 0x10, 0xe8, 0x07,
|
||||
0x12, 0x15, 0x0a, 0x10, 0x53, 0x61, 0x6c, 0x61, 0x64, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x41, 0x70,
|
||||
0x69, 0x4b, 0x65, 0x79, 0x10, 0xe9, 0x07, 0x12, 0x08, 0x0a, 0x03, 0x42, 0x6f, 0x78, 0x10, 0xea,
|
||||
0x07, 0x12, 0x0d, 0x0a, 0x08, 0x42, 0x6f, 0x78, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x10, 0xeb, 0x07,
|
||||
0x12, 0x0f, 0x0a, 0x0a, 0x41, 0x70, 0x69, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x10, 0xec,
|
||||
0x07, 0x12, 0x15, 0x0a, 0x10, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x73, 0x41, 0x6e, 0x64, 0x42,
|
||||
0x69, 0x61, 0x73, 0x65, 0x73, 0x10, 0xed, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x5a, 0x6f, 0x68, 0x6f,
|
||||
0x43, 0x52, 0x4d, 0x10, 0xee, 0x07, 0x12, 0x10, 0x0a, 0x0b, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4f,
|
||||
0x70, 0x65, 0x6e, 0x41, 0x49, 0x10, 0xef, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x47, 0x6f, 0x44, 0x61,
|
||||
0x64, 0x64, 0x79, 0x10, 0xf0, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x46, 0x6c, 0x65, 0x78, 0x70, 0x6f,
|
||||
0x72, 0x74, 0x10, 0xf1, 0x07, 0x12, 0x16, 0x0a, 0x11, 0x54, 0x77, 0x69, 0x74, 0x63, 0x68, 0x41,
|
||||
0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xf2, 0x07, 0x12, 0x11, 0x0a,
|
||||
0x0c, 0x54, 0x77, 0x69, 0x6c, 0x69, 0x6f, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x10, 0xf3, 0x07,
|
||||
0x12, 0x0b, 0x0a, 0x06, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x79, 0x10, 0xf4, 0x07, 0x12, 0x16, 0x0a,
|
||||
0x11, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b,
|
||||
0x65, 0x6e, 0x10, 0xf5, 0x07, 0x12, 0x12, 0x0a, 0x0d, 0x41, 0x69, 0x72, 0x74, 0x61, 0x62, 0x6c,
|
||||
0x65, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x10, 0xf6, 0x07, 0x12, 0x20, 0x0a, 0x1b, 0x41, 0x69, 0x72,
|
||||
0x74, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63,
|
||||
0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xf7, 0x07, 0x12, 0x21, 0x0a, 0x1c, 0x53,
|
||||
0x74, 0x6f, 0x72, 0x79, 0x62, 0x6c, 0x6f, 0x6b, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x6c,
|
||||
0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xf8, 0x07, 0x12, 0x13,
|
||||
0x0a, 0x0e, 0x53, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x4f, 0x72, 0x67, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
|
||||
0x10, 0xf9, 0x07, 0x12, 0x24, 0x0a, 0x1f, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4d,
|
||||
0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74,
|
||||
0x6f, 0x72, 0x79, 0x4b, 0x65, 0x79, 0x10, 0xfa, 0x07, 0x12, 0x26, 0x0a, 0x21, 0x41, 0x7a, 0x75,
|
||||
0x72, 0x65, 0x41, 0x50, 0x49, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53,
|
||||
0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xfb,
|
||||
0x07, 0x12, 0x0c, 0x0a, 0x07, 0x48, 0x61, 0x72, 0x6e, 0x65, 0x73, 0x73, 0x10, 0xfc, 0x07, 0x12,
|
||||
0x0d, 0x0a, 0x08, 0x4c, 0x61, 0x6e, 0x67, 0x66, 0x75, 0x73, 0x65, 0x10, 0xfd, 0x07, 0x12, 0x18,
|
||||
0x0a, 0x13, 0x42, 0x69, 0x6e, 0x67, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xfe, 0x07, 0x12, 0x08, 0x0a, 0x03, 0x58, 0x41, 0x49, 0x10,
|
||||
0xff, 0x07, 0x12, 0x1d, 0x0a, 0x18, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63,
|
||||
0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x10, 0x80,
|
||||
0x08, 0x12, 0x23, 0x0a, 0x1e, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x41, 0x70, 0x70, 0x43, 0x6f, 0x6e,
|
||||
0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72,
|
||||
0x69, 0x6e, 0x67, 0x10, 0x81, 0x08, 0x12, 0x0d, 0x0a, 0x08, 0x44, 0x65, 0x65, 0x70, 0x53, 0x65,
|
||||
0x65, 0x6b, 0x10, 0x82, 0x08, 0x12, 0x18, 0x0a, 0x13, 0x53, 0x74, 0x72, 0x69, 0x70, 0x65, 0x50,
|
||||
0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x10, 0x83, 0x08, 0x12,
|
||||
0x0e, 0x0a, 0x09, 0x4c, 0x61, 0x6e, 0x67, 0x53, 0x6d, 0x69, 0x74, 0x68, 0x10, 0x84, 0x08, 0x12,
|
||||
0x19, 0x0a, 0x14, 0x42, 0x69, 0x74, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x41, 0x70, 0x70, 0x50,
|
||||
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x10, 0x85, 0x08, 0x12, 0x0b, 0x0a, 0x06, 0x52, 0x6f,
|
||||
0x6f, 0x74, 0x6c, 0x79, 0x10, 0x86, 0x08, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75,
|
||||
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65, 0x63,
|
||||
0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f, 0x67,
|
||||
0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x63,
|
||||
0x74, 0x6f, 0x72, 0x73, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x08, 0x4d, 0x65, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x10, 0xb6, 0x07, 0x12, 0x15, 0x0a, 0x0c,
|
||||
0x43, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x57, 0x61, 0x61, 0x53, 0x10, 0xb7, 0x07, 0x1a,
|
||||
0x02, 0x08, 0x01, 0x12, 0x11, 0x0a, 0x0c, 0x4c, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x71, 0x75, 0x65,
|
||||
0x65, 0x7a, 0x79, 0x10, 0xb8, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x42, 0x75, 0x64, 0x69, 0x62, 0x61,
|
||||
0x73, 0x65, 0x10, 0xb9, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x44, 0x65, 0x6e, 0x6f, 0x44, 0x65, 0x70,
|
||||
0x6c, 0x6f, 0x79, 0x10, 0xba, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x53, 0x74, 0x72, 0x69, 0x70, 0x6f,
|
||||
0x10, 0xbb, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x49, 0x4f, 0x10, 0xbc,
|
||||
0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x10,
|
||||
0xbd, 0x07, 0x12, 0x1b, 0x0a, 0x16, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61,
|
||||
0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x10, 0xbe, 0x07, 0x12,
|
||||
0x12, 0x0a, 0x0d, 0x41, 0x57, 0x53, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79,
|
||||
0x10, 0xbf, 0x07, 0x12, 0x09, 0x0a, 0x04, 0x43, 0x6f, 0x64, 0x61, 0x10, 0xc0, 0x07, 0x12, 0x0b,
|
||||
0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x7a, 0x49, 0x4f, 0x10, 0xc1, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x45,
|
||||
0x76, 0x65, 0x6e, 0x74, 0x62, 0x72, 0x69, 0x74, 0x65, 0x10, 0xc2, 0x07, 0x12, 0x1a, 0x0a, 0x15,
|
||||
0x47, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63,
|
||||
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x10, 0xc3, 0x07, 0x12, 0x13, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x46, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x10, 0xc4, 0x07, 0x12, 0x0d, 0x0a,
|
||||
0x08, 0x4f, 0x76, 0x65, 0x72, 0x6c, 0x6f, 0x6f, 0x70, 0x10, 0xc5, 0x07, 0x12, 0x0a, 0x0a, 0x05,
|
||||
0x4e, 0x67, 0x72, 0x6f, 0x6b, 0x10, 0xc6, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x52, 0x65, 0x70, 0x6c,
|
||||
0x69, 0x63, 0x61, 0x74, 0x65, 0x10, 0xc7, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x50, 0x6f, 0x73, 0x74,
|
||||
0x67, 0x72, 0x65, 0x73, 0x10, 0xc8, 0x07, 0x12, 0x2a, 0x0a, 0x25, 0x41, 0x7a, 0x75, 0x72, 0x65,
|
||||
0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x41,
|
||||
0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74,
|
||||
0x10, 0xc9, 0x07, 0x12, 0x20, 0x0a, 0x1b, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x43, 0x61, 0x63, 0x68,
|
||||
0x65, 0x46, 0x6f, 0x72, 0x52, 0x65, 0x64, 0x69, 0x73, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b,
|
||||
0x65, 0x79, 0x10, 0xca, 0x07, 0x12, 0x21, 0x0a, 0x1c, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x43, 0x6f,
|
||||
0x73, 0x6d, 0x6f, 0x73, 0x44, 0x42, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66,
|
||||
0x69, 0x61, 0x62, 0x6c, 0x65, 0x10, 0xcb, 0x07, 0x12, 0x23, 0x0a, 0x1e, 0x41, 0x7a, 0x75, 0x72,
|
||||
0x65, 0x44, 0x65, 0x76, 0x6f, 0x70, 0x73, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x6c, 0x41,
|
||||
0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xcc, 0x07, 0x12, 0x15, 0x0a,
|
||||
0x10, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65,
|
||||
0x79, 0x10, 0xcd, 0x07, 0x12, 0x2c, 0x0a, 0x27, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4d, 0x4c, 0x57,
|
||||
0x65, 0x62, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x63,
|
||||
0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x10,
|
||||
0xce, 0x07, 0x12, 0x12, 0x0a, 0x0d, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x53, 0x61, 0x73, 0x54, 0x6f,
|
||||
0x6b, 0x65, 0x6e, 0x10, 0xcf, 0x07, 0x12, 0x18, 0x0a, 0x13, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x53,
|
||||
0x65, 0x61, 0x72, 0x63, 0x68, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xd0, 0x07,
|
||||
0x12, 0x18, 0x0a, 0x13, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x51,
|
||||
0x75, 0x65, 0x72, 0x79, 0x4b, 0x65, 0x79, 0x10, 0xd1, 0x07, 0x12, 0x1f, 0x0a, 0x1a, 0x41, 0x7a,
|
||||
0x75, 0x72, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72,
|
||||
0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x10, 0xd2, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x41,
|
||||
0x7a, 0x75, 0x72, 0x65, 0x53, 0x51, 0x4c, 0x10, 0xd3, 0x07, 0x12, 0x0a, 0x0a, 0x05, 0x46, 0x6c,
|
||||
0x79, 0x49, 0x4f, 0x10, 0xd4, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x42, 0x75, 0x69, 0x6c, 0x74, 0x57,
|
||||
0x69, 0x74, 0x68, 0x10, 0xd5, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x4a, 0x75, 0x70, 0x69, 0x74, 0x65,
|
||||
0x72, 0x4f, 0x6e, 0x65, 0x10, 0xd6, 0x07, 0x12, 0x25, 0x0a, 0x20, 0x47, 0x43, 0x50, 0x41, 0x70,
|
||||
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74,
|
||||
0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x10, 0xd7, 0x07, 0x12, 0x08,
|
||||
0x0a, 0x03, 0x57, 0x69, 0x7a, 0x10, 0xd8, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x50, 0x61, 0x67, 0x61,
|
||||
0x72, 0x6d, 0x65, 0x10, 0xd9, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x4f, 0x6e, 0x66, 0x6c, 0x65, 0x65,
|
||||
0x74, 0x10, 0xda, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x49, 0x6e, 0x74, 0x72, 0x61, 0x34, 0x32, 0x10,
|
||||
0xdb, 0x07, 0x12, 0x09, 0x0a, 0x04, 0x47, 0x72, 0x6f, 0x71, 0x10, 0xdc, 0x07, 0x12, 0x17, 0x0a,
|
||||
0x12, 0x54, 0x77, 0x69, 0x74, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x72,
|
||||
0x6b, 0x65, 0x79, 0x10, 0xdd, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x45, 0x72, 0x61, 0x73, 0x65, 0x72,
|
||||
0x10, 0xde, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x4c, 0x61, 0x72, 0x6b, 0x53, 0x75, 0x69, 0x74, 0x65,
|
||||
0x10, 0xdf, 0x07, 0x12, 0x14, 0x0a, 0x0f, 0x4c, 0x61, 0x72, 0x6b, 0x53, 0x75, 0x69, 0x74, 0x65,
|
||||
0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x10, 0xe0, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x45, 0x6e, 0x64,
|
||||
0x6f, 0x72, 0x4c, 0x61, 0x62, 0x73, 0x10, 0xe1, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x45, 0x6c, 0x65,
|
||||
0x76, 0x65, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x10, 0xe2, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x4e, 0x65,
|
||||
0x74, 0x73, 0x75, 0x69, 0x74, 0x65, 0x10, 0xe3, 0x07, 0x12, 0x14, 0x0a, 0x0f, 0x52, 0x6f, 0x62,
|
||||
0x69, 0x6e, 0x68, 0x6f, 0x6f, 0x64, 0x43, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x10, 0xe4, 0x07, 0x12,
|
||||
0x0a, 0x0a, 0x05, 0x4e, 0x56, 0x41, 0x50, 0x49, 0x10, 0xe5, 0x07, 0x12, 0x09, 0x0a, 0x04, 0x50,
|
||||
0x79, 0x50, 0x49, 0x10, 0xe6, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x52, 0x61, 0x69, 0x6c, 0x77, 0x61,
|
||||
0x79, 0x41, 0x70, 0x70, 0x10, 0xe7, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x4d, 0x65, 0x72, 0x61, 0x6b,
|
||||
0x69, 0x10, 0xe8, 0x07, 0x12, 0x15, 0x0a, 0x10, 0x53, 0x61, 0x6c, 0x61, 0x64, 0x43, 0x6c, 0x6f,
|
||||
0x75, 0x64, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x10, 0xe9, 0x07, 0x12, 0x08, 0x0a, 0x03, 0x42,
|
||||
0x6f, 0x78, 0x10, 0xea, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x42, 0x6f, 0x78, 0x4f, 0x61, 0x75, 0x74,
|
||||
0x68, 0x10, 0xeb, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x41, 0x70, 0x69, 0x4d, 0x65, 0x74, 0x72, 0x69,
|
||||
0x63, 0x73, 0x10, 0xec, 0x07, 0x12, 0x15, 0x0a, 0x10, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x73,
|
||||
0x41, 0x6e, 0x64, 0x42, 0x69, 0x61, 0x73, 0x65, 0x73, 0x10, 0xed, 0x07, 0x12, 0x0c, 0x0a, 0x07,
|
||||
0x5a, 0x6f, 0x68, 0x6f, 0x43, 0x52, 0x4d, 0x10, 0xee, 0x07, 0x12, 0x10, 0x0a, 0x0b, 0x41, 0x7a,
|
||||
0x75, 0x72, 0x65, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x10, 0xef, 0x07, 0x12, 0x0c, 0x0a, 0x07,
|
||||
0x47, 0x6f, 0x44, 0x61, 0x64, 0x64, 0x79, 0x10, 0xf0, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x46, 0x6c,
|
||||
0x65, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x10, 0xf1, 0x07, 0x12, 0x16, 0x0a, 0x11, 0x54, 0x77, 0x69,
|
||||
0x74, 0x63, 0x68, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xf2,
|
||||
0x07, 0x12, 0x11, 0x0a, 0x0c, 0x54, 0x77, 0x69, 0x6c, 0x69, 0x6f, 0x41, 0x70, 0x69, 0x4b, 0x65,
|
||||
0x79, 0x10, 0xf3, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x79, 0x10, 0xf4,
|
||||
0x07, 0x12, 0x16, 0x0a, 0x11, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73,
|
||||
0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xf5, 0x07, 0x12, 0x12, 0x0a, 0x0d, 0x41, 0x69, 0x72,
|
||||
0x74, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x10, 0xf6, 0x07, 0x12, 0x20, 0x0a,
|
||||
0x1b, 0x41, 0x69, 0x72, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61,
|
||||
0x6c, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xf7, 0x07, 0x12,
|
||||
0x21, 0x0a, 0x1c, 0x53, 0x74, 0x6f, 0x72, 0x79, 0x62, 0x6c, 0x6f, 0x6b, 0x50, 0x65, 0x72, 0x73,
|
||||
0x6f, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10,
|
||||
0xf8, 0x07, 0x12, 0x13, 0x0a, 0x0e, 0x53, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x4f, 0x72, 0x67, 0x54,
|
||||
0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xf9, 0x07, 0x12, 0x24, 0x0a, 0x1f, 0x41, 0x7a, 0x75, 0x72, 0x65,
|
||||
0x41, 0x70, 0x69, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x70,
|
||||
0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4b, 0x65, 0x79, 0x10, 0xfa, 0x07, 0x12, 0x26, 0x0a,
|
||||
0x21, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x41, 0x50, 0x49, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
|
||||
0x65, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b,
|
||||
0x65, 0x79, 0x10, 0xfb, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x48, 0x61, 0x72, 0x6e, 0x65, 0x73, 0x73,
|
||||
0x10, 0xfc, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x4c, 0x61, 0x6e, 0x67, 0x66, 0x75, 0x73, 0x65, 0x10,
|
||||
0xfd, 0x07, 0x12, 0x18, 0x0a, 0x13, 0x42, 0x69, 0x6e, 0x67, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72,
|
||||
0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xfe, 0x07, 0x12, 0x08, 0x0a, 0x03,
|
||||
0x58, 0x41, 0x49, 0x10, 0xff, 0x07, 0x12, 0x1d, 0x0a, 0x18, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x44,
|
||||
0x69, 0x72, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b,
|
||||
0x65, 0x79, 0x10, 0x80, 0x08, 0x12, 0x23, 0x0a, 0x1e, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x41, 0x70,
|
||||
0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x10, 0x81, 0x08, 0x12, 0x0d, 0x0a, 0x08, 0x44, 0x65,
|
||||
0x65, 0x70, 0x53, 0x65, 0x65, 0x6b, 0x10, 0x82, 0x08, 0x12, 0x18, 0x0a, 0x13, 0x53, 0x74, 0x72,
|
||||
0x69, 0x70, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74,
|
||||
0x10, 0x83, 0x08, 0x12, 0x0e, 0x0a, 0x09, 0x4c, 0x61, 0x6e, 0x67, 0x53, 0x6d, 0x69, 0x74, 0x68,
|
||||
0x10, 0x84, 0x08, 0x12, 0x19, 0x0a, 0x14, 0x42, 0x69, 0x74, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74,
|
||||
0x41, 0x70, 0x70, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x10, 0x85, 0x08, 0x12, 0x0b,
|
||||
0x0a, 0x06, 0x48, 0x61, 0x73, 0x75, 0x72, 0x61, 0x10, 0x86, 0x08, 0x12, 0x0b, 0x0a, 0x06, 0x52,
|
||||
0x6f, 0x6f, 0x74, 0x6c, 0x79, 0x10, 0x87, 0x08, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68,
|
||||
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65,
|
||||
0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f,
|
||||
0x67, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x64, 0x65, 0x74, 0x65,
|
||||
0x63, 0x74, 0x6f, 0x72, 0x73, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -587,13 +587,14 @@ func TestChunkUnit(t *testing.T) {
|
||||
}, &reporter)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Error path.
|
||||
// Error path - should return fatal error for missing directory.
|
||||
err = s.ChunkUnit(ctx, SourceUnit{
|
||||
ID: "/file/not/found",
|
||||
Kind: UnitDir,
|
||||
}, &reporter)
|
||||
assert.NoError(t, err)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "directory does not exist")
|
||||
|
||||
assert.Equal(t, 22, len(reporter.Chunks))
|
||||
assert.Equal(t, 1, len(reporter.ChunkErrs))
|
||||
assert.Equal(t, 0, len(reporter.ChunkErrs))
|
||||
}
|
||||
|
||||
@@ -97,6 +97,10 @@ func TestSource_Chunks(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.name == "gets chunks after assuming role" {
|
||||
t.Skip("skipping until our test environment stabilizes enough that we know how we're going to handle this")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
|
||||
+8
-1
@@ -143,10 +143,16 @@ func (ui *TUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch {
|
||||
case key.Matches(msg, ui.common.KeyMap.Help):
|
||||
case key.Matches(msg, ui.common.KeyMap.CmdQuit) && ui.activePage() != sourceConfigurePage:
|
||||
ui.args = nil
|
||||
return ui, tea.Quit
|
||||
case key.Matches(msg, ui.common.KeyMap.Quit):
|
||||
ui.args = nil
|
||||
return ui, tea.Quit
|
||||
case ui.activePage() > 0 && key.Matches(msg, ui.common.KeyMap.Back):
|
||||
case key.Matches(msg, ui.common.KeyMap.Back):
|
||||
if ui.activePage() == wizardIntroPage {
|
||||
ui.args = nil
|
||||
return ui, tea.Quit
|
||||
}
|
||||
_ = ui.popHistory()
|
||||
return ui, nil
|
||||
}
|
||||
@@ -159,6 +165,7 @@ func (ui *TUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
switch item {
|
||||
case wizard_intro.Quit:
|
||||
ui.args = nil
|
||||
cmds = append(cmds, tea.Quit)
|
||||
case wizard_intro.ViewOSSProject:
|
||||
ui.setActivePage(viewOSSProjectPage)
|
||||
|
||||
@@ -960,7 +960,7 @@ enum DetectorType {
|
||||
ZeroTier = 948;
|
||||
AppOptics = 949;
|
||||
Metabase = 950;
|
||||
CoinbaseWaaS = 951;
|
||||
CoinbaseWaaS = 951 [deprecated = true];
|
||||
LemonSqueezy = 952;
|
||||
Budibase = 953;
|
||||
DenoDeploy = 954;
|
||||
@@ -1039,7 +1039,8 @@ enum DetectorType {
|
||||
StripePaymentIntent = 1027;
|
||||
LangSmith = 1028;
|
||||
BitbucketAppPassword = 1029;
|
||||
Rootly = 1030;
|
||||
Hasura = 1030;
|
||||
Rootly = 1031;
|
||||
}
|
||||
|
||||
message Result {
|
||||
|
||||
Reference in New Issue
Block a user