Merge remote-tracking branch 'upstream/main' into detector-rootly

Signed-off-by: Sahil Silare <[email protected]>
This commit is contained in:
Sahil Silare
2025-06-29 20:56:04 +05:30
27 changed files with 1275 additions and 816 deletions
+63
View File
@@ -5,6 +5,7 @@ import (
"crypto/rand" "crypto/rand"
"io" "io"
"math/big" "math/big"
mrand "math/rand"
"strings" "strings"
) )
@@ -70,3 +71,65 @@ func SliceContainsString(origTargetString string, stringSlice []string, ignoreCa
} }
return false, "", 0 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)
}
+83
View File
@@ -5,6 +5,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"unicode"
) )
func TestAddItem(t *testing.T) { 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 ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
regexp "github.com/wasilibs/go-re2"
"io" "io"
"net/http" "net/http"
"slices" "slices"
"strings" "strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
"github.com/trufflesecurity/trufflehog/v3/pkg/common" "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/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" "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. // 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`) 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`) 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. // 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. // 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) { 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) dataStr := string(data)
// Deduplicate matches. // Deduplicate matches.
@@ -59,6 +68,12 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
// Test matches. // Test matches.
for key := range keyMatches { for key := range keyMatches {
for id := range idMatches { 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{ r := detectors.Result{
DetectorType: detectorspb.DetectorType_AlgoliaAdminKey, DetectorType: detectorspb.DetectorType_AlgoliaAdminKey,
Raw: []byte(key), 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) isVerified, extraData, verificationErr := verifyMatch(ctx, id, key)
r.Verified = isVerified r.Verified = isVerified
r.ExtraData = extraData 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) 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) res, err := client.Do(req)
if err != nil { 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 return false, nil, err
} }
defer func() { defer func() {
+71 -23
View File
@@ -2,12 +2,15 @@ package artifactory
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
regexp "github.com/wasilibs/go-re2" 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/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
) )
@@ -26,14 +29,20 @@ var (
defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives. // 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})`) 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)`) 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. // Keywords are used for efficiently pre-filtering chunks.
// Use identifiers in the secret preferably, or the provider name. // Use identifiers in the secret preferably, or the provider name.
func (s Scanner) Keywords() []string { func (s Scanner) Keywords() []string {
return []string{"artifactory"} return []string{"artifactory", "jfrog.io"}
} }
func (s Scanner) getClient() *http.Client { 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. // 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) { func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data) dataStr := string(data)
URLmatches := URLPat.FindAllStringSubmatch(dataStr, -1)
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
resURLMatch := "" var uniqueTokens, uniqueUrls = make(map[string]struct{}), make(map[string]struct{})
for _, URLmatch := range URLmatches {
resURLMatch = strings.TrimSpace(URLmatch[1]) for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueTokens[match[1]] = struct{}{}
} }
for _, match := range matches { var foundUrls = make([]string, 0)
resMatch := strings.TrimSpace(match[1])
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{ s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_ArtifactoryAccessToken, DetectorType: detectorspb.DetectorType_ArtifactoryAccessToken,
Raw: []byte(resMatch), Raw: []byte(token),
RawV2: []byte(resMatch + URL), RawV2: []byte(token + url),
} }
if verify { if verify {
isVerified, verificationErr := verifyArtifactory(ctx, client, URL, resMatch) isVerified, verificationErr := verifyArtifactory(ctx, s.getClient(), url, token)
s1.Verified = isVerified 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) 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) { 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 { if err != nil {
return false, err return false, err
} }
req.Header.Add("X-JFrog-Art-Api", resMatch) req.Header.Add("X-JFrog-Art-Api", resMatch)
res, err := client.Do(req)
resp, err := client.Do(req)
if err != nil { if err != nil {
// lookup foo.jfrog.io: no such host
if strings.Contains(err.Error(), "no such host") {
return false, errNoHost
}
return false, err 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: case http.StatusOK:
return true, nil body, err := io.ReadAll(resp.Body)
case http.StatusForbidden: 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 // https://jfrog.com/help/r/jfrog-rest-apis/error-responses
return false, nil return false, nil
default: 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, 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", name: "found, unverified",
s: Scanner{}, s: Scanner{},
@@ -122,6 +86,8 @@ func TestArtifactory_FromChunk(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("Artifactory.FromData() error = %v, wantErr %v", err, 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()) 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 != "" { if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Artifactory.FromData() %s diff: (-got +want)\n%s", tt.name, diff) t.Errorf("Artifactory.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
} }
+61 -16
View File
@@ -2,7 +2,6 @@ package artifactory
import ( import (
"context" "context"
"fmt"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
@@ -11,13 +10,6 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick" "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) { func TestArtifactory_Pattern(t *testing.T) {
d := Scanner{} d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d}) ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
@@ -31,23 +23,76 @@ func TestArtifactory_Pattern(t *testing.T) {
want []string want []string
}{ }{
{ {
name: "valid pattern", name: "valid pattern",
input: fmt.Sprintf("artifactory credentials: %s", validPattern), input: `
# artifactory credentials
Token: cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZg
Url: rwxtOp.jfrog.io
`,
useCloudEndpoint: false, useCloudEndpoint: false,
useFoundEndpoint: true, useFoundEndpoint: true,
want: []string{"5YcZhIKwxTdxwpZHf9c1Usu8xNtAklRsqWYXWf2qmjW0RSQQ0U4sVnrNgOwIJlTOqJf06T3dlRVo8ytzB65L.jfrog.io"}, want: []string{"cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZgrwxtOp.jfrog.io"},
}, },
{ {
name: "valid pattern - with cloud endpoints", name: "valid pattern - with cloud endpoints",
input: fmt.Sprintf("artifactory credentials: %s", validPattern2), input: `
# artifactory credentials
Token: cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZg
`,
cloudEndpoint: "cloudendpoint.jfrog.io", cloudEndpoint: "cloudendpoint.jfrog.io",
useCloudEndpoint: true, useCloudEndpoint: true,
useFoundEndpoint: false, useFoundEndpoint: false,
want: []string{"5YcZhIKwxTdxwpZHf9c1Usu8xNtAklRsqWYXWf2qmjW0RSQQ0U4sVnrNgOwIJlTOqJf06T3dlcloudendpoint.jfrog.io"}, want: []string{"cmVmdGtuOjAxOjE3ODA1NTFAKEM6S2J2MGswemNzZzhaRnFlVUFAKEk3amlLcGZgcloudendpoint.jfrog.io"},
}, },
{ {
name: "invalid pattern", name: "valid pattern - with cloud and found endpoints",
input: fmt.Sprintf("artifactory credentials: %s", invalidPattern), 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, useFoundEndpoint: true,
want: nil, want: nil,
}, },
+66 -34
View File
@@ -2,6 +2,7 @@ package billomat
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -25,8 +26,10 @@ var (
client = common.SaneHttpClient() client = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives. // 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]{4,20})\b`) // the Billomat ID must be between 4 and 20 characters long.
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"billomat"}) + `\b([0-9a-z]{1,})\b`) 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. // Keywords are used for efficiently pre-filtering chunks.
@@ -35,37 +38,6 @@ func (s Scanner) Keywords() []string {
return []string{"billomat"} 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 { func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Billomat 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." 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/ // docs: https://www.billomat.com/en/api/basics/authentication/
func verifyBillomat(ctx context.Context, client *http.Client, id, key string) (bool, error) { 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 { if err != nil {
return false, err return false, err
} }
@@ -98,6 +113,23 @@ func verifyBillomat(ctx context.Context, client *http.Client, id, key string) (b
case http.StatusOK: case http.StatusOK:
return true, nil return true, nil
case http.StatusUnauthorized: 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 return false, nil
default: default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 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]) t.Fatalf("no raw secret present: \n %+v", got[i])
} }
got[i].Raw = nil got[i].Raw = nil
got[i].RawV2 = nil
} }
if diff := pretty.Compare(got, tt.want); diff != "" { if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Billomat.FromData() %s diff: (-got +want)\n%s", tt.name, diff) t.Errorf("Billomat.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
+33 -46
View File
@@ -10,37 +10,6 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick" "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) { func TestBilloMat_Pattern(t *testing.T) {
d := Scanner{} d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d}) ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
@@ -51,25 +20,43 @@ func TestBilloMat_Pattern(t *testing.T) {
want []string want []string
}{ }{
{ {
name: "valid pattern", name: "valid pattern",
input: validPattern, 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{ want: []string{
"xv3khh5klgzztdmptrgbqhkr0ucvr67is2mels7c75tnsbs7ldu0wmjofzmugkg7vb", "c09761f99f39f79ae28eaaf8df20d7c9truffletest",
"xv3khh5klgzztdmptrgbqhkr0ucvr67ixv3khh5klgzztdmptrgbqhkr0ucvr67i",
}, },
}, },
{ {
name: "valid pattern - complex", name: "invalid pattern",
input: complexPattern, input: `
want: []string{ req.Header.Set("X-BillomatApiKey", "c09761h99f39f79ae28eaaf8df20d7c9")
"xv3khh5klgzztdmptrgbqhkr0ucvr67inet", billomatID := truffle-test
"xv3khh5klgzztdmptrgbqhkr0ucvr67ixv3khh5klgzztdmptrgbqhkr0ucvr67i", `,
}, want: nil,
},
{
name: "invalid pattern",
input: invalidPattern,
want: nil,
}, },
} }
+4 -4
View File
@@ -7,15 +7,15 @@ import (
"github.com/google/go-cmp/cmp" "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/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick" "github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
) )
var ( var (
clientId = gofakeit.Password(true, true, true, false, false, 32) clientId = common.GenerateRandomPassword(true, true, true, false, 32)
clientSecret = gofakeit.Password(true, true, true, false, false, 32) clientSecret = common.GenerateRandomPassword(true, true, true, false, 32)
invalidClientSecret = gofakeit.Password(true, true, true, true, false, 32) invalidClientSecret = common.GenerateRandomPassword(true, true, true, true, 32)
) )
func TestBoxOauth_Pattern(t *testing.T) { func TestBoxOauth_Pattern(t *testing.T) {
+157 -26
View File
@@ -2,74 +2,205 @@ package coinbase
import ( import (
"context" "context"
"crypto/ecdsa"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time"
regexp "github.com/wasilibs/go-re2" regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" "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. // Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil) var _ detectors.Detector = (*Scanner)(nil)
var ( var (
client = common.SaneHttpClient() defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives. // Reference: https://docs.cdp.coinbase.com/coinbase-app/docs/auth/api-key-authentication
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"coinbase"}) + `\b([a-zA-Z-0-9]{64})\b`) 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. // Keywords are used for efficiently pre-filtering chunks.
// Use identifiers in the secret preferably, or the provider name. // Use identifiers in the secret preferably, or the provider name.
func (s Scanner) Keywords() []string { 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. // 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) { func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data) dataStr := string(data)
matches := keyPat.FindAllStringSubmatch(dataStr, -1) uniqueKeyNames, uniquePrivateKeys := map[string]struct{}{}, map[string]struct{}{}
for _, match := range matches { for _, keyNameMatch := range keyNamePat.FindAllStringSubmatch(dataStr, -1) {
resMatch := strings.TrimSpace(match[1]) uniqueKeyNames[keyNameMatch[1]] = struct{}{}
}
s1 := detectors.Result{ for _, privateKeyMatch := range privateKeyPat.FindAllStringSubmatch(dataStr, -1) {
DetectorType: detectorspb.DetectorType_Coinbase, uniquePrivateKeys[privateKeyMatch[1]] = struct{}{}
Raw: []byte(resMatch), }
}
if verify { for keyName := range uniqueKeyNames {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.coinbase.com/v2/user", nil) for privateKey := range uniquePrivateKeys {
if err != nil { client := s.getClient()
resKeyName := nameReplacer.Replace(strings.TrimSpace(keyName))
resPrivateKey := keyReplacer.Replace(strings.TrimSpace(privateKey))
if !isValidECPrivateKey([]byte(resPrivateKey)) {
continue 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 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 { func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Coinbase return detectorspb.DetectorType_Coinbase
} }
func (s Scanner) Description() string { 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 ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"testing" "testing"
"time" "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/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
) )
func TestCoinbase_FromChunk(t *testing.T) { func TestCoinbase_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel() defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3") testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
if err != nil { if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err) 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 { type args struct {
ctx context.Context ctx context.Context
@@ -32,18 +38,19 @@ func TestCoinbase_FromChunk(t *testing.T) {
verify bool verify bool
} }
tests := []struct { tests := []struct {
name string name string
s Scanner s Scanner
args args args args
want []detectors.Result want []detectors.Result
wantErr bool wantErr bool
wantVerificationErr bool
}{ }{
{ {
name: "found, verified", name: "found, verified",
s: Scanner{}, s: Scanner{},
args: args{ args: args{
ctx: context.Background(), 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, verify: true,
}, },
want: []detectors.Result{ want: []detectors.Result{
@@ -52,14 +59,15 @@ func TestCoinbase_FromChunk(t *testing.T) {
Verified: true, Verified: true,
}, },
}, },
wantErr: false, wantErr: false,
wantVerificationErr: false,
}, },
{ {
name: "found, unverified", name: "found, unverified",
s: Scanner{}, s: Scanner{},
args: args{ args: args{
ctx: context.Background(), 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, verify: true,
}, },
want: []detectors.Result{ want: []detectors.Result{
@@ -68,7 +76,8 @@ func TestCoinbase_FromChunk(t *testing.T) {
Verified: false, Verified: false,
}, },
}, },
wantErr: false, wantErr: false,
wantVerificationErr: false,
}, },
{ {
name: "not found", name: "not found",
@@ -78,25 +87,62 @@ func TestCoinbase_FromChunk(t *testing.T) {
data: []byte("You cannot find the secret within"), data: []byte("You cannot find the secret within"),
verify: true, verify: true,
}, },
want: nil, want: nil,
wantErr: false, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
s := Scanner{} got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("Coinbase.FromData() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("Coinbase.FromData() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
for i := range got { for i := range got {
if len(got[i].Raw) == 0 { 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) t.Errorf("Coinbase.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
} }
}) })
@@ -108,8 +154,12 @@ func BenchmarkFromData(benchmark *testing.B) {
s := Scanner{} s := Scanner{}
for name, data := range detectors.MustGetBenchmarkData() { for name, data := range detectors.MustGetBenchmarkData() {
benchmark.Run(name, func(b *testing.B) { benchmark.Run(name, func(b *testing.B) {
b.ResetTimer()
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
s.FromData(ctx, false, data) _, err := s.FromData(ctx, false, data)
if err != nil {
b.Fatal(err)
}
} }
}) })
} }
+109 -64
View File
@@ -3,88 +3,133 @@ package coinbase
import ( import (
"context" "context"
"testing" "testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
) )
var ( func TestCoinbase_Pattern(t *testing.T) {
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})
tests := []struct { tests := []struct {
name string name string
input string data string
want []string shouldMatch bool
match string
}{ }{
// True positives
// https://github.com/coinbase/waas-client-library-go/issues/41
{ {
name: "valid pattern", name: "valid_result1",
input: validPattern, 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" }`,
want: []string{secret}, 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 { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input)) s := Scanner{}
if len(matchedDetectors) == 0 {
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input)) results, err := s.FromData(context.Background(), false, []byte(test.data))
if err != nil { if err != nil {
t.Errorf("error = %v", err) t.Errorf("Coinbase.FromData() error = %v", err)
return return
} }
if len(results) != len(test.want) { if test.shouldMatch {
if len(results) == 0 { if len(results) == 0 {
t.Errorf("did not receive result") t.Errorf("%s: did not receive a match for '%v' when one was expected", test.name, test.data)
} else { return
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
} }
return expected := test.data
} if test.match != "" {
expected = test.match
actual := make(map[string]struct{}, len(results)) }
for _, r := range results { result := results[0]
if len(r.RawV2) > 0 { resultData := string(result.RawV2)
actual[string(r.RawV2)] = struct{}{} if resultData != expected {
} else { t.Errorf("%s: did not receive expected match.\n\texpected: '%s'\n\t actual: '%s'", test.name, expected, resultData)
actual[string(r.Raw)] = struct{}{} 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
}
}
})
}
}
+157
View File
@@ -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"))
}
@@ -1,11 +1,10 @@
//go:build detectors //go:build detectors
// +build detectors // +build detectors
package coinbase_waas package hasura
import ( import (
"context" "context"
"encoding/base64"
"fmt" "fmt"
"testing" "testing"
"time" "time"
@@ -13,32 +12,22 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "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/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" "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) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel() defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5") testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
if err != nil { if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err) 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") secret := testSecrets.MustGetField("HASURA")
inactiveSecretB, err := base64.StdEncoding.DecodeString(inactiveSecretb64) inactiveSecret := testSecrets.MustGetField("HASURA_INACTIVE")
if err != nil { domain := testSecrets.MustGetField("HASURA_DOMAIN")
t.Fatalf("could not decode secret: %s", err)
}
inactiveSecret := string(inactiveSecretB)
type args struct { type args struct {
ctx context.Context ctx context.Context
@@ -58,13 +47,16 @@ func TestCoinbaseWaaS_FromChunk(t *testing.T) {
s: Scanner{}, s: Scanner{},
args: args{ args: args{
ctx: context.Background(), 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, verify: true,
}, },
want: []detectors.Result{ want: []detectors.Result{
{ {
DetectorType: detectorspb.DetectorType_CoinbaseWaaS, DetectorType: detectorspb.DetectorType_Hasura,
Verified: true, Verified: true,
Raw: []byte(secret),
RawV2: []byte(fmt.Sprintf("%s:%s", domain, secret)),
ExtraData: map[string]string{"domain": domain},
}, },
}, },
wantErr: false, wantErr: false,
@@ -75,13 +67,16 @@ func TestCoinbaseWaaS_FromChunk(t *testing.T) {
s: Scanner{}, s: Scanner{},
args: args{ args: args{
ctx: context.Background(), 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, verify: true,
}, },
want: []detectors.Result{ want: []detectors.Result{
{ {
DetectorType: detectorspb.DetectorType_CoinbaseWaaS, DetectorType: detectorspb.DetectorType_Hasura,
Verified: false, Verified: false,
Raw: []byte(inactiveSecret),
RawV2: []byte(fmt.Sprintf("%s:%s", domain, inactiveSecret)),
ExtraData: map[string]string{"domain": domain},
}, },
}, },
wantErr: false, wantErr: false,
@@ -104,30 +99,36 @@ func TestCoinbaseWaaS_FromChunk(t *testing.T) {
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)}, s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
args: args{ args: args{
ctx: context.Background(), 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, verify: true,
}, },
want: []detectors.Result{ want: []detectors.Result{
{ {
DetectorType: detectorspb.DetectorType_CoinbaseWaaS, DetectorType: detectorspb.DetectorType_Hasura,
Verified: false, Verified: false,
Raw: []byte(secret),
RawV2: []byte(fmt.Sprintf("%s:%s", domain, secret)),
ExtraData: nil,
}, },
}, },
wantErr: false, wantErr: false,
wantVerificationErr: true, wantVerificationErr: true,
}, },
{ {
name: "found, verified but unexpected api surface", name: "found, unexpected api response",
s: Scanner{client: common.ConstantResponseHttpClient(500, "")}, s: Scanner{client: common.ConstantResponseHttpClient(500, "")},
args: args{ args: args{
ctx: context.Background(), 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, verify: true,
}, },
want: []detectors.Result{ want: []detectors.Result{
{ {
DetectorType: detectorspb.DetectorType_CoinbaseWaaS, DetectorType: detectorspb.DetectorType_Hasura,
Verified: false, Verified: false,
Raw: []byte(secret),
RawV2: []byte(fmt.Sprintf("%s:%s", domain, secret)),
ExtraData: map[string]string{"domain": domain},
}, },
}, },
wantErr: false, wantErr: false,
@@ -138,20 +139,21 @@ func TestCoinbaseWaaS_FromChunk(t *testing.T) {
t.Run(tt.name, func(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) got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr { 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 return
} }
for i := range got { for i := range got {
if len(got[i].Raw) == 0 { if len(got[i].Raw) == 0 {
t.Fatalf("no raw secret present: \n %+v", got[i]) t.Fatalf("no raw secret present: \n %+v", got[i])
} }
if (got[i].VerificationError() != nil) != tt.wantVerificationErr { 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") ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "verificationError", "primarySecret")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" { if diff := cmp.Diff(tt.want, got, ignoreOpts); diff != "" {
t.Errorf("Coinbasewaas.FromData() %s diff: (-got +want)\n%s", tt.name, diff) t.Errorf("Hasura.FromData() %s - diff: (-got +want)\n%s", tt.name, diff)
} }
}) })
} }
+136
View File
@@ -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)
}
})
}
}
+2 -53
View File
@@ -3,66 +3,15 @@ package snowflake
import ( import (
"context" "context"
"fmt" "fmt"
"math/rand"
"testing" "testing"
"github.com/brianvoe/gofakeit/v7" "github.com/brianvoe/gofakeit/v7"
"github.com/google/go-cmp/cmp" "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/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick" "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) { func TestSnowflake_Pattern(t *testing.T) {
validAccount := "tuacoip-zt74995" validAccount := "tuacoip-zt74995"
@@ -71,7 +20,7 @@ func TestSnowflake_Pattern(t *testing.T) {
validUsername := gofakeit.Username() validUsername := gofakeit.Username()
invalidUsername := "[email protected]" // special characters not allowed 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 invalidPassword := "!12" // invalid length
d := Scanner{} d := Scanner{}
+13 -6
View File
@@ -3,6 +3,7 @@ package sqlserver
import ( import (
"context" "context"
"database/sql" "database/sql"
"fmt"
"net" "net"
"strconv" "strconv"
"time" "time"
@@ -57,12 +58,18 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
isVerified, err := ping(ctx, paramsUnsafe) isVerified, err := ping(ctx, paramsUnsafe)
s1.Verified = isVerified s1.Verified = isVerified
mssqlErr, isMssqlErr := err.(mssql.Error)
if mssqlErr, isMssqlErr := err.(mssql.Error); isMssqlErr && mssqlErr.Number == 18456 { if isMssqlErr {
// Login failed if mssqlErr.Number == 18456 {
// Number taken from https://learn.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors?view=sql-server-ver16 // Login failed
// Nothing to do; determinate failure to verify // Number taken from https://learn.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors?view=sql-server-ver16
} else { // 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) s1.SetVerificationError(err, paramsUnsafe.Password)
} }
} }
+5 -4
View File
@@ -47,6 +47,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/appoptics" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/appoptics"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/appsynergy" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/appsynergy"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/apptivo" "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/artsy"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/asanaoauth" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/asanaoauth"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/asanapersonalaccesstoken" "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/codequiry"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/coinapi" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/coinapi"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/coinbase" "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/coinlayer"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/coinlib" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/coinlib"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/collect2" "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/happyscribe"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/harness" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/harness"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/harvest" "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/hellosign"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/helpcrunch" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/helpcrunch"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/helpscout" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/helpscout"
@@ -898,7 +899,7 @@ func buildDetectorList() []detectors.Detector {
&appoptics.Scanner{}, &appoptics.Scanner{},
&appsynergy.Scanner{}, &appsynergy.Scanner{},
&apptivo.Scanner{}, &apptivo.Scanner{},
// &artifactory.Scanner{}, &artifactory.Scanner{},
&artsy.Scanner{}, &artsy.Scanner{},
&asanaoauth.Scanner{}, &asanaoauth.Scanner{},
&asanapersonalaccesstoken.Scanner{}, &asanapersonalaccesstoken.Scanner{},
@@ -1027,7 +1028,6 @@ func buildDetectorList() []detectors.Detector {
&codequiry.Scanner{}, &codequiry.Scanner{},
&coinapi.Scanner{}, &coinapi.Scanner{},
&coinbase.Scanner{}, &coinbase.Scanner{},
&coinbase_waas.Scanner{},
&coinlayer.Scanner{}, &coinlayer.Scanner{},
&coinlib.Scanner{}, &coinlib.Scanner{},
&collect2.Scanner{}, &collect2.Scanner{},
@@ -1481,6 +1481,7 @@ func buildDetectorList() []detectors.Detector {
&rocketreach.Scanner{}, &rocketreach.Scanner{},
// &rockset.Scanner{}, // &rockset.Scanner{},
&roninapp.Scanner{}, &roninapp.Scanner{},
&rootly.Scanner{},
&route4me.Scanner{}, &route4me.Scanner{},
&rownd.Scanner{}, &rownd.Scanner{},
&rubygems.Scanner{}, &rubygems.Scanner{},
@@ -1717,7 +1718,7 @@ func buildDetectorList() []detectors.Detector {
&zulipchat.Scanner{}, &zulipchat.Scanner{},
&stripepaymentintent.Scanner{}, &stripepaymentintent.Scanner{},
&bitbucketapppassword.Scanner{}, &bitbucketapppassword.Scanner{},
&rootly.Scanner{} &hasura.Scanner{},
} }
} }
+1 -1
View File
@@ -1228,7 +1228,7 @@ func TestEngineInitializesCloudProviderDetectors(t *testing.T) {
for _, det := range e.detectors { for _, det := range e.detectors {
if endpoints, ok := det.(interface{ Endpoints(...string) []string }); ok { if endpoints, ok := det.(interface{ Endpoints(...string) []string }); ok {
id := config.GetDetectorID(det) 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()) t.Fatalf("detector %q Endpoints() is empty", id.String())
} }
count++ count++
+157 -152
View File
@@ -1015,46 +1015,47 @@ const (
DetectorType_Moralis DetectorType = 909 DetectorType_Moralis DetectorType = 909
DetectorType_BscScan DetectorType = 910 DetectorType_BscScan DetectorType = 910
// Deprecated: Marked as deprecated in detectors.proto. // Deprecated: Marked as deprecated in detectors.proto.
DetectorType_CoinMarketCap DetectorType = 911 DetectorType_CoinMarketCap DetectorType = 911
DetectorType_Percy DetectorType = 912 DetectorType_Percy DetectorType = 912
DetectorType_TinesWebhook DetectorType = 913 DetectorType_TinesWebhook DetectorType = 913
DetectorType_Pulumi DetectorType = 914 DetectorType_Pulumi DetectorType = 914
DetectorType_SupabaseToken DetectorType = 915 DetectorType_SupabaseToken DetectorType = 915
DetectorType_NuGetApiKey DetectorType = 916 DetectorType_NuGetApiKey DetectorType = 916
DetectorType_Aiven DetectorType = 917 DetectorType_Aiven DetectorType = 917
DetectorType_Prefect DetectorType = 918 DetectorType_Prefect DetectorType = 918
DetectorType_Docusign DetectorType = 919 DetectorType_Docusign DetectorType = 919
DetectorType_Couchbase DetectorType = 920 DetectorType_Couchbase DetectorType = 920
DetectorType_Dockerhub DetectorType = 921 DetectorType_Dockerhub DetectorType = 921
DetectorType_TrufflehogEnterprise DetectorType = 922 DetectorType_TrufflehogEnterprise DetectorType = 922
DetectorType_EnvoyApiKey DetectorType = 923 DetectorType_EnvoyApiKey DetectorType = 923
DetectorType_GitHubOauth2 DetectorType = 924 DetectorType_GitHubOauth2 DetectorType = 924
DetectorType_Salesforce DetectorType = 925 DetectorType_Salesforce DetectorType = 925
DetectorType_HuggingFace DetectorType = 926 DetectorType_HuggingFace DetectorType = 926
DetectorType_Snowflake DetectorType = 927 DetectorType_Snowflake DetectorType = 927
DetectorType_Sourcegraph DetectorType = 928 DetectorType_Sourcegraph DetectorType = 928
DetectorType_Tailscale DetectorType = 929 DetectorType_Tailscale DetectorType = 929
DetectorType_Web3Storage DetectorType = 930 DetectorType_Web3Storage DetectorType = 930
DetectorType_AzureStorage DetectorType = 931 DetectorType_AzureStorage DetectorType = 931
DetectorType_PlanetScaleDb DetectorType = 932 DetectorType_PlanetScaleDb DetectorType = 932
DetectorType_Anthropic DetectorType = 933 DetectorType_Anthropic DetectorType = 933
DetectorType_Ramp DetectorType = 934 DetectorType_Ramp DetectorType = 934
DetectorType_Klaviyo DetectorType = 935 DetectorType_Klaviyo DetectorType = 935
DetectorType_SourcegraphCody DetectorType = 936 DetectorType_SourcegraphCody DetectorType = 936
DetectorType_Voiceflow DetectorType = 937 DetectorType_Voiceflow DetectorType = 937
DetectorType_Privacy DetectorType = 938 DetectorType_Privacy DetectorType = 938
DetectorType_IPInfo DetectorType = 939 DetectorType_IPInfo DetectorType = 939
DetectorType_Ip2location DetectorType = 940 DetectorType_Ip2location DetectorType = 940
DetectorType_Instamojo DetectorType = 941 DetectorType_Instamojo DetectorType = 941
DetectorType_Portainer DetectorType = 942 DetectorType_Portainer DetectorType = 942
DetectorType_PortainerToken DetectorType = 943 DetectorType_PortainerToken DetectorType = 943
DetectorType_Loggly DetectorType = 944 DetectorType_Loggly DetectorType = 944
DetectorType_OpenVpn DetectorType = 945 DetectorType_OpenVpn DetectorType = 945
DetectorType_VagrantCloudPersonalToken DetectorType = 946 DetectorType_VagrantCloudPersonalToken DetectorType = 946
DetectorType_BetterStack DetectorType = 947 DetectorType_BetterStack DetectorType = 947
DetectorType_ZeroTier DetectorType = 948 DetectorType_ZeroTier DetectorType = 948
DetectorType_AppOptics DetectorType = 949 DetectorType_AppOptics DetectorType = 949
DetectorType_Metabase DetectorType = 950 DetectorType_Metabase DetectorType = 950
// Deprecated: Marked as deprecated in detectors.proto.
DetectorType_CoinbaseWaaS DetectorType = 951 DetectorType_CoinbaseWaaS DetectorType = 951
DetectorType_LemonSqueezy DetectorType = 952 DetectorType_LemonSqueezy DetectorType = 952
DetectorType_Budibase DetectorType = 953 DetectorType_Budibase DetectorType = 953
@@ -1134,7 +1135,8 @@ const (
DetectorType_StripePaymentIntent DetectorType = 1027 DetectorType_StripePaymentIntent DetectorType = 1027
DetectorType_LangSmith DetectorType = 1028 DetectorType_LangSmith DetectorType = 1028
DetectorType_BitbucketAppPassword DetectorType = 1029 DetectorType_BitbucketAppPassword DetectorType = 1029
DetectorType_Rootly DetectorType = 1030 DetectorType_Hasura DetectorType = 1030
DetectorType_Rootly DetectorType = 1031
) )
// Enum value maps for DetectorType. // Enum value maps for DetectorType.
@@ -2166,7 +2168,8 @@ var (
1027: "StripePaymentIntent", 1027: "StripePaymentIntent",
1028: "LangSmith", 1028: "LangSmith",
1029: "BitbucketAppPassword", 1029: "BitbucketAppPassword",
1030: "Rootly", 1030: "Hasura",
1031: "Rootly",
} }
DetectorType_value = map[string]int32{ DetectorType_value = map[string]int32{
"Alibaba": 0, "Alibaba": 0,
@@ -3195,7 +3198,8 @@ var (
"StripePaymentIntent": 1027, "StripePaymentIntent": 1027,
"LangSmith": 1028, "LangSmith": 1028,
"BitbucketAppPassword": 1029, "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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 0x12, 0x43, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x57, 0x61, 0x61, 0x53, 0x10, 0xb7, 0x07, 0x1a,
0x11, 0x0a, 0x0c, 0x4c, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x71, 0x75, 0x65, 0x65, 0x7a, 0x79, 0x10, 0x02, 0x08, 0x01, 0x12, 0x11, 0x0a, 0x0c, 0x4c, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x71, 0x75, 0x65,
0xb8, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x42, 0x75, 0x64, 0x69, 0x62, 0x61, 0x73, 0x65, 0x10, 0xb9, 0x65, 0x7a, 0x79, 0x10, 0xb8, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x42, 0x75, 0x64, 0x69, 0x62, 0x61,
0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x44, 0x65, 0x6e, 0x6f, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x10, 0x73, 0x65, 0x10, 0xb9, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x44, 0x65, 0x6e, 0x6f, 0x44, 0x65, 0x70,
0xba, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x53, 0x74, 0x72, 0x69, 0x70, 0x6f, 0x10, 0xbb, 0x07, 0x12, 0x6c, 0x6f, 0x79, 0x10, 0xba, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x53, 0x74, 0x72, 0x69, 0x70, 0x6f,
0x0c, 0x0a, 0x07, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x49, 0x4f, 0x10, 0xbc, 0x07, 0x12, 0x0f, 0x0a, 0x10, 0xbb, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x49, 0x4f, 0x10, 0xbc,
0x0a, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x10, 0xbd, 0x07, 0x12, 0x1b, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x42, 0x61, 0x74, 0x63, 0x68, 0x10,
0x0a, 0x16, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0xbd, 0x07, 0x12, 0x1b, 0x0a, 0x16, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x10, 0xbe, 0x07, 0x12, 0x12, 0x0a, 0x0d, 0x41, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x10, 0xbe, 0x07, 0x12,
0x57, 0x53, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xbf, 0x07, 0x12, 0x12, 0x0a, 0x0d, 0x41, 0x57, 0x53, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79,
0x09, 0x0a, 0x04, 0x43, 0x6f, 0x64, 0x61, 0x10, 0xc0, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x4c, 0x6f, 0x10, 0xbf, 0x07, 0x12, 0x09, 0x0a, 0x04, 0x43, 0x6f, 0x64, 0x61, 0x10, 0xc0, 0x07, 0x12, 0x0b,
0x67, 0x7a, 0x49, 0x4f, 0x10, 0xc1, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x7a, 0x49, 0x4f, 0x10, 0xc1, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x45,
0x62, 0x72, 0x69, 0x74, 0x65, 0x10, 0xc2, 0x07, 0x12, 0x1a, 0x0a, 0x15, 0x47, 0x72, 0x61, 0x66, 0x76, 0x65, 0x6e, 0x74, 0x62, 0x72, 0x69, 0x74, 0x65, 0x10, 0xc2, 0x07, 0x12, 0x1a, 0x0a, 0x15,
0x61, 0x6e, 0x61, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x47, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63,
0x74, 0x10, 0xc3, 0x07, 0x12, 0x13, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x46, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x10, 0xc3, 0x07, 0x12, 0x13, 0x0a, 0x0e, 0x52, 0x65, 0x71, 0x75,
0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x10, 0xc4, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x4f, 0x76, 0x65, 0x65, 0x73, 0x74, 0x46, 0x69, 0x6e, 0x61, 0x6e, 0x63, 0x65, 0x10, 0xc4, 0x07, 0x12, 0x0d, 0x0a,
0x72, 0x6c, 0x6f, 0x6f, 0x70, 0x10, 0xc5, 0x07, 0x12, 0x0a, 0x0a, 0x05, 0x4e, 0x67, 0x72, 0x6f, 0x08, 0x4f, 0x76, 0x65, 0x72, 0x6c, 0x6f, 0x6f, 0x70, 0x10, 0xc5, 0x07, 0x12, 0x0a, 0x0a, 0x05,
0x6b, 0x10, 0xc6, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x4e, 0x67, 0x72, 0x6f, 0x6b, 0x10, 0xc6, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x52, 0x65, 0x70, 0x6c,
0x65, 0x10, 0xc7, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x50, 0x6f, 0x73, 0x74, 0x67, 0x72, 0x65, 0x73, 0x69, 0x63, 0x61, 0x74, 0x65, 0x10, 0xc7, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x50, 0x6f, 0x73, 0x74,
0x10, 0xc8, 0x07, 0x12, 0x2a, 0x0a, 0x25, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x41, 0x63, 0x74, 0x69, 0x67, 0x72, 0x65, 0x73, 0x10, 0xc8, 0x07, 0x12, 0x2a, 0x0a, 0x25, 0x41, 0x7a, 0x75, 0x72, 0x65,
0x76, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x41,
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x10, 0xc9, 0x07, 0x12, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74,
0x20, 0x0a, 0x1b, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x43, 0x61, 0x63, 0x68, 0x65, 0x46, 0x6f, 0x72, 0x10, 0xc9, 0x07, 0x12, 0x20, 0x0a, 0x1b, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x43, 0x61, 0x63, 0x68,
0x52, 0x65, 0x64, 0x69, 0x73, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x10, 0xca, 0x65, 0x46, 0x6f, 0x72, 0x52, 0x65, 0x64, 0x69, 0x73, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b,
0x07, 0x12, 0x21, 0x0a, 0x1c, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x65, 0x79, 0x10, 0xca, 0x07, 0x12, 0x21, 0x0a, 0x1c, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x43, 0x6f,
0x44, 0x42, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x73, 0x6d, 0x6f, 0x73, 0x44, 0x42, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66,
0x65, 0x10, 0xcb, 0x07, 0x12, 0x23, 0x0a, 0x1e, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x44, 0x65, 0x76, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x10, 0xcb, 0x07, 0x12, 0x23, 0x0a, 0x1e, 0x41, 0x7a, 0x75, 0x72,
0x6f, 0x70, 0x73, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x65, 0x73, 0x65, 0x44, 0x65, 0x76, 0x6f, 0x70, 0x73, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x6c, 0x41,
0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xcc, 0x07, 0x12, 0x15, 0x0a, 0x10, 0x41, 0x7a, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xcc, 0x07, 0x12, 0x15, 0x0a,
0x72, 0x65, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xcd, 0x07, 0x10, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65,
0x12, 0x2c, 0x0a, 0x27, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4d, 0x4c, 0x57, 0x65, 0x62, 0x53, 0x65, 0x79, 0x10, 0xcd, 0x07, 0x12, 0x2c, 0x0a, 0x27, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4d, 0x4c, 0x57,
0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x63, 0x49, 0x64, 0x65, 0x6e, 0x65, 0x62, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x63,
0x74, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x10, 0xce, 0x07, 0x12, 0x12, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x4b, 0x65, 0x79, 0x10,
0x0a, 0x0d, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x53, 0x61, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xce, 0x07, 0x12, 0x12, 0x0a, 0x0d, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x53, 0x61, 0x73, 0x54, 0x6f,
0xcf, 0x07, 0x12, 0x18, 0x0a, 0x13, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x6b, 0x65, 0x6e, 0x10, 0xcf, 0x07, 0x12, 0x18, 0x0a, 0x13, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x53,
0x68, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xd0, 0x07, 0x12, 0x18, 0x0a, 0x13, 0x65, 0x61, 0x72, 0x63, 0x68, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xd0, 0x07,
0x41, 0x7a, 0x75, 0x72, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x13, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x51,
0x4b, 0x65, 0x79, 0x10, 0xd1, 0x07, 0x12, 0x1f, 0x0a, 0x1a, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4d, 0x75, 0x65, 0x72, 0x79, 0x4b, 0x65, 0x79, 0x10, 0xd1, 0x07, 0x12, 0x1f, 0x0a, 0x1a, 0x41, 0x7a,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x75, 0x72, 0x65, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x43, 0x65, 0x72,
0x63, 0x61, 0x74, 0x65, 0x10, 0xd2, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x10, 0xd2, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x41,
0x53, 0x51, 0x4c, 0x10, 0xd3, 0x07, 0x12, 0x0a, 0x0a, 0x05, 0x46, 0x6c, 0x79, 0x49, 0x4f, 0x10, 0x7a, 0x75, 0x72, 0x65, 0x53, 0x51, 0x4c, 0x10, 0xd3, 0x07, 0x12, 0x0a, 0x0a, 0x05, 0x46, 0x6c,
0xd4, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x42, 0x75, 0x69, 0x6c, 0x74, 0x57, 0x69, 0x74, 0x68, 0x10, 0x79, 0x49, 0x4f, 0x10, 0xd4, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x42, 0x75, 0x69, 0x6c, 0x74, 0x57,
0xd5, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x4a, 0x75, 0x70, 0x69, 0x74, 0x65, 0x72, 0x4f, 0x6e, 0x65, 0x69, 0x74, 0x68, 0x10, 0xd5, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x4a, 0x75, 0x70, 0x69, 0x74, 0x65,
0x10, 0xd6, 0x07, 0x12, 0x25, 0x0a, 0x20, 0x47, 0x43, 0x50, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x72, 0x4f, 0x6e, 0x65, 0x10, 0xd6, 0x07, 0x12, 0x25, 0x0a, 0x20, 0x47, 0x43, 0x50, 0x41, 0x70,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x72, 0x65, 0x64, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74,
0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x10, 0xd7, 0x07, 0x12, 0x08, 0x0a, 0x03, 0x57, 0x69, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x10, 0xd7, 0x07, 0x12, 0x08,
0x7a, 0x10, 0xd8, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x50, 0x61, 0x67, 0x61, 0x72, 0x6d, 0x65, 0x10, 0x0a, 0x03, 0x57, 0x69, 0x7a, 0x10, 0xd8, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x50, 0x61, 0x67, 0x61,
0xd9, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x4f, 0x6e, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x10, 0xda, 0x07, 0x72, 0x6d, 0x65, 0x10, 0xd9, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x4f, 0x6e, 0x66, 0x6c, 0x65, 0x65,
0x12, 0x0c, 0x0a, 0x07, 0x49, 0x6e, 0x74, 0x72, 0x61, 0x34, 0x32, 0x10, 0xdb, 0x07, 0x12, 0x09, 0x74, 0x10, 0xda, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x49, 0x6e, 0x74, 0x72, 0x61, 0x34, 0x32, 0x10,
0x0a, 0x04, 0x47, 0x72, 0x6f, 0x71, 0x10, 0xdc, 0x07, 0x12, 0x17, 0x0a, 0x12, 0x54, 0x77, 0x69, 0xdb, 0x07, 0x12, 0x09, 0x0a, 0x04, 0x47, 0x72, 0x6f, 0x71, 0x10, 0xdc, 0x07, 0x12, 0x17, 0x0a,
0x74, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x72, 0x6b, 0x65, 0x79, 0x10, 0x12, 0x54, 0x77, 0x69, 0x74, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x72,
0xdd, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x45, 0x72, 0x61, 0x73, 0x65, 0x72, 0x10, 0xde, 0x07, 0x12, 0x6b, 0x65, 0x79, 0x10, 0xdd, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x45, 0x72, 0x61, 0x73, 0x65, 0x72,
0x0e, 0x0a, 0x09, 0x4c, 0x61, 0x72, 0x6b, 0x53, 0x75, 0x69, 0x74, 0x65, 0x10, 0xdf, 0x07, 0x12, 0x10, 0xde, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x4c, 0x61, 0x72, 0x6b, 0x53, 0x75, 0x69, 0x74, 0x65,
0x14, 0x0a, 0x0f, 0x4c, 0x61, 0x72, 0x6b, 0x53, 0x75, 0x69, 0x74, 0x65, 0x41, 0x70, 0x69, 0x4b, 0x10, 0xdf, 0x07, 0x12, 0x14, 0x0a, 0x0f, 0x4c, 0x61, 0x72, 0x6b, 0x53, 0x75, 0x69, 0x74, 0x65,
0x65, 0x79, 0x10, 0xe0, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x45, 0x6e, 0x64, 0x6f, 0x72, 0x4c, 0x61, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x10, 0xe0, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x45, 0x6e, 0x64,
0x62, 0x73, 0x10, 0xe1, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x45, 0x6c, 0x65, 0x76, 0x65, 0x6e, 0x4c, 0x6f, 0x72, 0x4c, 0x61, 0x62, 0x73, 0x10, 0xe1, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x45, 0x6c, 0x65,
0x61, 0x62, 0x73, 0x10, 0xe2, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x4e, 0x65, 0x74, 0x73, 0x75, 0x69, 0x76, 0x65, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x10, 0xe2, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x4e, 0x65,
0x74, 0x65, 0x10, 0xe3, 0x07, 0x12, 0x14, 0x0a, 0x0f, 0x52, 0x6f, 0x62, 0x69, 0x6e, 0x68, 0x6f, 0x74, 0x73, 0x75, 0x69, 0x74, 0x65, 0x10, 0xe3, 0x07, 0x12, 0x14, 0x0a, 0x0f, 0x52, 0x6f, 0x62,
0x6f, 0x64, 0x43, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x10, 0xe4, 0x07, 0x12, 0x0a, 0x0a, 0x05, 0x4e, 0x69, 0x6e, 0x68, 0x6f, 0x6f, 0x64, 0x43, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x10, 0xe4, 0x07, 0x12,
0x56, 0x41, 0x50, 0x49, 0x10, 0xe5, 0x07, 0x12, 0x09, 0x0a, 0x04, 0x50, 0x79, 0x50, 0x49, 0x10, 0x0a, 0x0a, 0x05, 0x4e, 0x56, 0x41, 0x50, 0x49, 0x10, 0xe5, 0x07, 0x12, 0x09, 0x0a, 0x04, 0x50,
0xe6, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x52, 0x61, 0x69, 0x6c, 0x77, 0x61, 0x79, 0x41, 0x70, 0x70, 0x79, 0x50, 0x49, 0x10, 0xe6, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x52, 0x61, 0x69, 0x6c, 0x77, 0x61,
0x10, 0xe7, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x4d, 0x65, 0x72, 0x61, 0x6b, 0x69, 0x10, 0xe8, 0x07, 0x79, 0x41, 0x70, 0x70, 0x10, 0xe7, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x4d, 0x65, 0x72, 0x61, 0x6b,
0x12, 0x15, 0x0a, 0x10, 0x53, 0x61, 0x6c, 0x61, 0x64, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x41, 0x70, 0x69, 0x10, 0xe8, 0x07, 0x12, 0x15, 0x0a, 0x10, 0x53, 0x61, 0x6c, 0x61, 0x64, 0x43, 0x6c, 0x6f,
0x69, 0x4b, 0x65, 0x79, 0x10, 0xe9, 0x07, 0x12, 0x08, 0x0a, 0x03, 0x42, 0x6f, 0x78, 0x10, 0xea, 0x75, 0x64, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x10, 0xe9, 0x07, 0x12, 0x08, 0x0a, 0x03, 0x42,
0x07, 0x12, 0x0d, 0x0a, 0x08, 0x42, 0x6f, 0x78, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x10, 0xeb, 0x07, 0x6f, 0x78, 0x10, 0xea, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x42, 0x6f, 0x78, 0x4f, 0x61, 0x75, 0x74,
0x12, 0x0f, 0x0a, 0x0a, 0x41, 0x70, 0x69, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x10, 0xec, 0x68, 0x10, 0xeb, 0x07, 0x12, 0x0f, 0x0a, 0x0a, 0x41, 0x70, 0x69, 0x4d, 0x65, 0x74, 0x72, 0x69,
0x07, 0x12, 0x15, 0x0a, 0x10, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x73, 0x41, 0x6e, 0x64, 0x42, 0x63, 0x73, 0x10, 0xec, 0x07, 0x12, 0x15, 0x0a, 0x10, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x73,
0x69, 0x61, 0x73, 0x65, 0x73, 0x10, 0xed, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x5a, 0x6f, 0x68, 0x6f, 0x41, 0x6e, 0x64, 0x42, 0x69, 0x61, 0x73, 0x65, 0x73, 0x10, 0xed, 0x07, 0x12, 0x0c, 0x0a, 0x07,
0x43, 0x52, 0x4d, 0x10, 0xee, 0x07, 0x12, 0x10, 0x0a, 0x0b, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x4f, 0x5a, 0x6f, 0x68, 0x6f, 0x43, 0x52, 0x4d, 0x10, 0xee, 0x07, 0x12, 0x10, 0x0a, 0x0b, 0x41, 0x7a,
0x70, 0x65, 0x6e, 0x41, 0x49, 0x10, 0xef, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x47, 0x6f, 0x44, 0x61, 0x75, 0x72, 0x65, 0x4f, 0x70, 0x65, 0x6e, 0x41, 0x49, 0x10, 0xef, 0x07, 0x12, 0x0c, 0x0a, 0x07,
0x64, 0x64, 0x79, 0x10, 0xf0, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x46, 0x6c, 0x65, 0x78, 0x70, 0x6f, 0x47, 0x6f, 0x44, 0x61, 0x64, 0x64, 0x79, 0x10, 0xf0, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x46, 0x6c,
0x72, 0x74, 0x10, 0xf1, 0x07, 0x12, 0x16, 0x0a, 0x11, 0x54, 0x77, 0x69, 0x74, 0x63, 0x68, 0x41, 0x65, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x10, 0xf1, 0x07, 0x12, 0x16, 0x0a, 0x11, 0x54, 0x77, 0x69,
0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xf2, 0x07, 0x12, 0x11, 0x0a, 0x74, 0x63, 0x68, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xf2,
0x0c, 0x54, 0x77, 0x69, 0x6c, 0x69, 0x6f, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x10, 0xf3, 0x07, 0x07, 0x12, 0x11, 0x0a, 0x0c, 0x54, 0x77, 0x69, 0x6c, 0x69, 0x6f, 0x41, 0x70, 0x69, 0x4b, 0x65,
0x12, 0x0b, 0x0a, 0x06, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x79, 0x10, 0xf4, 0x07, 0x12, 0x16, 0x0a, 0x79, 0x10, 0xf3, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x53, 0x61, 0x6e, 0x69, 0x74, 0x79, 0x10, 0xf4,
0x11, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x07, 0x12, 0x16, 0x0a, 0x11, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73,
0x65, 0x6e, 0x10, 0xf5, 0x07, 0x12, 0x12, 0x0a, 0x0d, 0x41, 0x69, 0x72, 0x74, 0x61, 0x62, 0x6c, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xf5, 0x07, 0x12, 0x12, 0x0a, 0x0d, 0x41, 0x69, 0x72,
0x65, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x10, 0xf6, 0x07, 0x12, 0x20, 0x0a, 0x1b, 0x41, 0x69, 0x72, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x10, 0xf6, 0x07, 0x12, 0x20, 0x0a,
0x74, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x1b, 0x41, 0x69, 0x72, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61,
0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xf7, 0x07, 0x12, 0x21, 0x0a, 0x1c, 0x53, 0x6c, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xf7, 0x07, 0x12,
0x74, 0x6f, 0x72, 0x79, 0x62, 0x6c, 0x6f, 0x6b, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x61, 0x6c, 0x21, 0x0a, 0x1c, 0x53, 0x74, 0x6f, 0x72, 0x79, 0x62, 0x6c, 0x6f, 0x6b, 0x50, 0x65, 0x72, 0x73,
0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xf8, 0x07, 0x12, 0x13, 0x6f, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10,
0x0a, 0x0e, 0x53, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x4f, 0x72, 0x67, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0xf8, 0x07, 0x12, 0x13, 0x0a, 0x0e, 0x53, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x4f, 0x72, 0x67, 0x54,
0x10, 0xf9, 0x07, 0x12, 0x24, 0x0a, 0x1f, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x41, 0x70, 0x69, 0x4d, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xf9, 0x07, 0x12, 0x24, 0x0a, 0x1f, 0x41, 0x7a, 0x75, 0x72, 0x65,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x41, 0x70, 0x69, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x70,
0x6f, 0x72, 0x79, 0x4b, 0x65, 0x79, 0x10, 0xfa, 0x07, 0x12, 0x26, 0x0a, 0x21, 0x41, 0x7a, 0x75, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x4b, 0x65, 0x79, 0x10, 0xfa, 0x07, 0x12, 0x26, 0x0a,
0x72, 0x65, 0x41, 0x50, 0x49, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x21, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x41, 0x50, 0x49, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xfb, 0x65, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b,
0x07, 0x12, 0x0c, 0x0a, 0x07, 0x48, 0x61, 0x72, 0x6e, 0x65, 0x73, 0x73, 0x10, 0xfc, 0x07, 0x12, 0x65, 0x79, 0x10, 0xfb, 0x07, 0x12, 0x0c, 0x0a, 0x07, 0x48, 0x61, 0x72, 0x6e, 0x65, 0x73, 0x73,
0x0d, 0x0a, 0x08, 0x4c, 0x61, 0x6e, 0x67, 0x66, 0x75, 0x73, 0x65, 0x10, 0xfd, 0x07, 0x12, 0x18, 0x10, 0xfc, 0x07, 0x12, 0x0d, 0x0a, 0x08, 0x4c, 0x61, 0x6e, 0x67, 0x66, 0x75, 0x73, 0x65, 0x10,
0x0a, 0x13, 0x42, 0x69, 0x6e, 0x67, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0xfd, 0x07, 0x12, 0x18, 0x0a, 0x13, 0x42, 0x69, 0x6e, 0x67, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72,
0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xfe, 0x07, 0x12, 0x08, 0x0a, 0x03, 0x58, 0x41, 0x49, 0x10, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x10, 0xfe, 0x07, 0x12, 0x08, 0x0a, 0x03,
0xff, 0x07, 0x12, 0x1d, 0x0a, 0x18, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x58, 0x41, 0x49, 0x10, 0xff, 0x07, 0x12, 0x1d, 0x0a, 0x18, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x44,
0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x10, 0x80, 0x69, 0x72, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b,
0x08, 0x12, 0x23, 0x0a, 0x1e, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x41, 0x70, 0x70, 0x43, 0x6f, 0x6e, 0x65, 0x79, 0x10, 0x80, 0x08, 0x12, 0x23, 0x0a, 0x1e, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x41, 0x70,
0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x72, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f,
0x69, 0x6e, 0x67, 0x10, 0x81, 0x08, 0x12, 0x0d, 0x0a, 0x08, 0x44, 0x65, 0x65, 0x70, 0x53, 0x65, 0x6e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x10, 0x81, 0x08, 0x12, 0x0d, 0x0a, 0x08, 0x44, 0x65,
0x65, 0x6b, 0x10, 0x82, 0x08, 0x12, 0x18, 0x0a, 0x13, 0x53, 0x74, 0x72, 0x69, 0x70, 0x65, 0x50, 0x65, 0x70, 0x53, 0x65, 0x65, 0x6b, 0x10, 0x82, 0x08, 0x12, 0x18, 0x0a, 0x13, 0x53, 0x74, 0x72,
0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x10, 0x83, 0x08, 0x12, 0x69, 0x70, 0x65, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74,
0x0e, 0x0a, 0x09, 0x4c, 0x61, 0x6e, 0x67, 0x53, 0x6d, 0x69, 0x74, 0x68, 0x10, 0x84, 0x08, 0x12, 0x10, 0x83, 0x08, 0x12, 0x0e, 0x0a, 0x09, 0x4c, 0x61, 0x6e, 0x67, 0x53, 0x6d, 0x69, 0x74, 0x68,
0x19, 0x0a, 0x14, 0x42, 0x69, 0x74, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x41, 0x70, 0x70, 0x50, 0x10, 0x84, 0x08, 0x12, 0x19, 0x0a, 0x14, 0x42, 0x69, 0x74, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74,
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x10, 0x85, 0x08, 0x12, 0x0b, 0x0a, 0x06, 0x52, 0x6f, 0x41, 0x70, 0x70, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x10, 0x85, 0x08, 0x12, 0x0b,
0x6f, 0x74, 0x6c, 0x79, 0x10, 0x86, 0x08, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x0a, 0x06, 0x48, 0x61, 0x73, 0x75, 0x72, 0x61, 0x10, 0x86, 0x08, 0x12, 0x0b, 0x0a, 0x06, 0x52,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65, 0x63, 0x6f, 0x6f, 0x74, 0x6c, 0x79, 0x10, 0x87, 0x08, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68,
0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f, 0x67, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65,
0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f,
0x74, 0x6f, 0x72, 0x73, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 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 ( var (
+4 -3
View File
@@ -587,13 +587,14 @@ func TestChunkUnit(t *testing.T) {
}, &reporter) }, &reporter)
assert.NoError(t, err) assert.NoError(t, err)
// Error path. // Error path - should return fatal error for missing directory.
err = s.ChunkUnit(ctx, SourceUnit{ err = s.ChunkUnit(ctx, SourceUnit{
ID: "/file/not/found", ID: "/file/not/found",
Kind: UnitDir, Kind: UnitDir,
}, &reporter) }, &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, 22, len(reporter.Chunks))
assert.Equal(t, 1, len(reporter.ChunkErrs)) assert.Equal(t, 0, len(reporter.ChunkErrs))
} }
+4
View File
@@ -97,6 +97,10 @@ func TestSource_Chunks(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel() defer cancel()
+8 -1
View File
@@ -143,10 +143,16 @@ func (ui *TUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch { switch {
case key.Matches(msg, ui.common.KeyMap.Help): case key.Matches(msg, ui.common.KeyMap.Help):
case key.Matches(msg, ui.common.KeyMap.CmdQuit) && ui.activePage() != sourceConfigurePage: case key.Matches(msg, ui.common.KeyMap.CmdQuit) && ui.activePage() != sourceConfigurePage:
ui.args = nil
return ui, tea.Quit return ui, tea.Quit
case key.Matches(msg, ui.common.KeyMap.Quit): case key.Matches(msg, ui.common.KeyMap.Quit):
ui.args = nil
return ui, tea.Quit 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() _ = ui.popHistory()
return ui, nil return ui, nil
} }
@@ -159,6 +165,7 @@ func (ui *TUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch item { switch item {
case wizard_intro.Quit: case wizard_intro.Quit:
ui.args = nil
cmds = append(cmds, tea.Quit) cmds = append(cmds, tea.Quit)
case wizard_intro.ViewOSSProject: case wizard_intro.ViewOSSProject:
ui.setActivePage(viewOSSProjectPage) ui.setActivePage(viewOSSProjectPage)
+3 -2
View File
@@ -960,7 +960,7 @@ enum DetectorType {
ZeroTier = 948; ZeroTier = 948;
AppOptics = 949; AppOptics = 949;
Metabase = 950; Metabase = 950;
CoinbaseWaaS = 951; CoinbaseWaaS = 951 [deprecated = true];
LemonSqueezy = 952; LemonSqueezy = 952;
Budibase = 953; Budibase = 953;
DenoDeploy = 954; DenoDeploy = 954;
@@ -1039,7 +1039,8 @@ enum DetectorType {
StripePaymentIntent = 1027; StripePaymentIntent = 1027;
LangSmith = 1028; LangSmith = 1028;
BitbucketAppPassword = 1029; BitbucketAppPassword = 1029;
Rootly = 1030; Hasura = 1030;
Rootly = 1031;
} }
message Result { message Result {