Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 070cacfc49 | |||
| 6082e0c458 | |||
| 7ac7fa8728 | |||
| 4b821e9732 | |||
| 758344711a | |||
| 8058006a92 | |||
| 3d7207ddd5 | |||
| b57b1c1aa7 | |||
| a354cbd796 | |||
| 23ae970bb0 | |||
| 93cf523760 |
@@ -47,6 +47,7 @@ require (
|
|||||||
github.com/h2non/filetype v1.1.3
|
github.com/h2non/filetype v1.1.3
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.4
|
github.com/hashicorp/go-retryablehttp v0.7.4
|
||||||
github.com/hashicorp/golang-lru v0.5.1
|
github.com/hashicorp/golang-lru v0.5.1
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||||
github.com/jlaffaye/ftp v0.2.0
|
github.com/jlaffaye/ftp v0.2.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/jpillora/overseer v1.1.6
|
github.com/jpillora/overseer v1.1.6
|
||||||
@@ -81,6 +82,7 @@ require (
|
|||||||
google.golang.org/protobuf v1.31.0
|
google.golang.org/protobuf v1.31.0
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||||
gopkg.in/h2non/gock.v1 v1.1.2
|
gopkg.in/h2non/gock.v1 v1.1.2
|
||||||
|
pgregory.net/rapid v1.1.0
|
||||||
sigs.k8s.io/yaml v1.3.0
|
sigs.k8s.io/yaml v1.3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -418,6 +418,8 @@ github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5
|
|||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||||
@@ -1013,6 +1015,8 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
|
|||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
|
pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw=
|
||||||
|
pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gobwas/glob"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter is a generic filter for excluding and including globs (limited
|
||||||
|
// regular expressions). Exclusion takes precedence if both include and exclude
|
||||||
|
// lists are provided.
|
||||||
|
type Filter struct {
|
||||||
|
exclude []glob.Glob
|
||||||
|
include []glob.Glob
|
||||||
|
}
|
||||||
|
|
||||||
|
type globFilterOpt func(*Filter) error
|
||||||
|
|
||||||
|
// WithExcludeGlobs adds exclude globs to the filter.
|
||||||
|
func WithExcludeGlobs(excludes ...string) globFilterOpt {
|
||||||
|
return func(f *Filter) error {
|
||||||
|
for _, exclude := range excludes {
|
||||||
|
g, err := glob.Compile(exclude)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid exclude glob %q: %w", exclude, err)
|
||||||
|
}
|
||||||
|
f.exclude = append(f.exclude, g)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIncludeGlobs adds include globs to the filter.
|
||||||
|
func WithIncludeGlobs(includes ...string) globFilterOpt {
|
||||||
|
return func(f *Filter) error {
|
||||||
|
for _, include := range includes {
|
||||||
|
g, err := glob.Compile(include)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid include glob %q: %w", include, err)
|
||||||
|
}
|
||||||
|
f.include = append(f.include, g)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGlobFilter creates a new Filter with the provided options.
|
||||||
|
func NewGlobFilter(opts ...globFilterOpt) (*Filter, error) {
|
||||||
|
filter := &Filter{}
|
||||||
|
for _, opt := range opts {
|
||||||
|
if err := opt(filter); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldInclude returns whether the object is in the include list or not in
|
||||||
|
// the exclude list (exclude taking precedence).
|
||||||
|
func (f *Filter) ShouldInclude(object string) bool {
|
||||||
|
if f == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
exclude, include := len(f.exclude), len(f.include)
|
||||||
|
if exclude == 0 && include == 0 {
|
||||||
|
return true
|
||||||
|
} else if exclude > 0 && include == 0 {
|
||||||
|
return f.shouldIncludeFromExclude(object)
|
||||||
|
} else if exclude == 0 && include > 0 {
|
||||||
|
return f.shouldIncludeFromInclude(object)
|
||||||
|
} else {
|
||||||
|
if ok, err := f.shouldIncludeFromBoth(object); err == nil {
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
// Ambiguous case.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldIncludeFromExclude checks for explicitly excluded paths. This should
|
||||||
|
// only be called when the include list is empty.
|
||||||
|
func (f *Filter) shouldIncludeFromExclude(object string) bool {
|
||||||
|
for _, glob := range f.exclude {
|
||||||
|
if glob.Match(object) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldIncludeFromInclude checks for explicitly included paths. This should
|
||||||
|
// only be called when the exclude list is empty.
|
||||||
|
func (f *Filter) shouldIncludeFromInclude(object string) bool {
|
||||||
|
for _, glob := range f.include {
|
||||||
|
if glob.Match(object) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldIncludeFromBoth checks for either excluded or included paths. Exclusion
|
||||||
|
// takes precedence. If neither list contains the object, true is returned.
|
||||||
|
func (f *Filter) shouldIncludeFromBoth(object string) (bool, error) {
|
||||||
|
// Exclude takes precedence. If we find the object in the exclude list,
|
||||||
|
// we should not match.
|
||||||
|
for _, glob := range f.exclude {
|
||||||
|
if glob.Match(object) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we find the object in the include list, we should match.
|
||||||
|
for _, glob := range f.include {
|
||||||
|
if glob.Match(object) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we find it in neither, return an error to let the caller decide.
|
||||||
|
return false, fmt.Errorf("ambiguous match")
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"pgregory.net/rapid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type globTest struct {
|
||||||
|
input string
|
||||||
|
shouldInclude bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGlobs(t *testing.T, filter *Filter, tests ...globTest) {
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
// Invert because mentally it's easier to say whether an
|
||||||
|
// input should be included.
|
||||||
|
assert.Equal(t, tt.shouldInclude, filter.ShouldInclude(tt.input))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobFilterExclude(t *testing.T) {
|
||||||
|
filter, err := NewGlobFilter(WithExcludeGlobs("foo", "bar*"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
testGlobs(t, filter,
|
||||||
|
globTest{"foo", false},
|
||||||
|
globTest{"bar", false},
|
||||||
|
globTest{"bara", false},
|
||||||
|
globTest{"barb", false},
|
||||||
|
globTest{"barbosa", false},
|
||||||
|
globTest{"foobar", true},
|
||||||
|
globTest{"food", true},
|
||||||
|
globTest{"anything else", true},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobFilterInclude(t *testing.T) {
|
||||||
|
filter, err := NewGlobFilter(WithIncludeGlobs("foo", "bar*"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
testGlobs(t, filter,
|
||||||
|
globTest{"foo", true},
|
||||||
|
globTest{"bar", true},
|
||||||
|
globTest{"bara", true},
|
||||||
|
globTest{"barb", true},
|
||||||
|
globTest{"barbosa", true},
|
||||||
|
globTest{"foobar", false},
|
||||||
|
globTest{"food", false},
|
||||||
|
globTest{"anything else", false},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobFilterEmpty(t *testing.T) {
|
||||||
|
filter, err := NewGlobFilter()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
testGlobs(t, filter,
|
||||||
|
globTest{"foo", true},
|
||||||
|
globTest{"bar", true},
|
||||||
|
globTest{"bara", true},
|
||||||
|
globTest{"barb", true},
|
||||||
|
globTest{"barbosa", true},
|
||||||
|
globTest{"foobar", true},
|
||||||
|
globTest{"food", true},
|
||||||
|
globTest{"anything else", true},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobFilterExcludeInclude(t *testing.T) {
|
||||||
|
filter, err := NewGlobFilter(WithExcludeGlobs("/foo/bar/**"), WithIncludeGlobs("/foo/**"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
testGlobs(t, filter,
|
||||||
|
globTest{"/foo/a", true},
|
||||||
|
globTest{"/foo/b", true},
|
||||||
|
globTest{"/foo/c/d/e", true},
|
||||||
|
globTest{"/foo/bar/a", false},
|
||||||
|
globTest{"/foo/bar/b", false},
|
||||||
|
globTest{"/foo/bar/c/d/e", false},
|
||||||
|
globTest{"/any/other/path", false},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobFilterExcludePrecedence(t *testing.T) {
|
||||||
|
filter, err := NewGlobFilter(WithExcludeGlobs("foo"), WithIncludeGlobs("foo*"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
testGlobs(t, filter,
|
||||||
|
globTest{"foo", false},
|
||||||
|
globTest{"foobar", true},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobErrorContainsGlob(t *testing.T) {
|
||||||
|
invalidGlob := "[this is invalid because it doesn't close the capture group"
|
||||||
|
_, err := NewGlobFilter(WithExcludeGlobs(invalidGlob))
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), invalidGlob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The filters in this test should be mutually exclusive because one includes
|
||||||
|
// and the other excludes the same glob.
|
||||||
|
func TestGlobInverse(t *testing.T) {
|
||||||
|
for _, glob := range []string{
|
||||||
|
"a",
|
||||||
|
"a*",
|
||||||
|
"a**",
|
||||||
|
"*a",
|
||||||
|
"**a",
|
||||||
|
"*",
|
||||||
|
} {
|
||||||
|
include, err := NewGlobFilter(WithIncludeGlobs(glob))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
exclude, err := NewGlobFilter(WithExcludeGlobs(glob))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
rapid.Check(t, func(t *rapid.T) {
|
||||||
|
input := rapid.String().Draw(t, "input")
|
||||||
|
a, b := include.ShouldInclude(input), exclude.ShouldInclude(input)
|
||||||
|
if a == b {
|
||||||
|
t.Fatalf("Filter(Include(%q)) == Filter(Exclude(%q)) == %v for input %q", glob, glob, a, input)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobDefaultFilters(t *testing.T) {
|
||||||
|
for _, filter := range []*Filter{nil, {}} {
|
||||||
|
rapid.Check(t, func(t *rapid.T) {
|
||||||
|
if !filter.ShouldInclude(rapid.String().Draw(t, "input")) {
|
||||||
|
t.Fatalf("filter %#v did not include input", filter)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,7 +97,6 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
s1.ExtraData["site_admin"] = fmt.Sprintf("%t", userResponse.SiteAdmin)
|
s1.ExtraData["site_admin"] = fmt.Sprintf("%t", userResponse.SiteAdmin)
|
||||||
s1.ExtraData["name"] = userResponse.Name
|
s1.ExtraData["name"] = userResponse.Name
|
||||||
s1.ExtraData["company"] = userResponse.Company
|
s1.ExtraData["company"] = userResponse.Company
|
||||||
s1.ExtraData["scopes"] = res.Header.Get("X-OAuth-Scopes")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ func TestGitHub_FromChunk(t *testing.T) {
|
|||||||
unverifiedGhu := testSecrets.MustGetField("GITHUB_UNVERIFIED_GHU")
|
unverifiedGhu := testSecrets.MustGetField("GITHUB_UNVERIFIED_GHU")
|
||||||
unverifiedGhs := testSecrets.MustGetField("GITHUB_UNVERIFIED_GHS")
|
unverifiedGhs := testSecrets.MustGetField("GITHUB_UNVERIFIED_GHS")
|
||||||
unverifiedGhr := testSecrets.MustGetField("GITHUB_UNVERIFIED_GHR")
|
unverifiedGhr := testSecrets.MustGetField("GITHUB_UNVERIFIED_GHR")
|
||||||
verifiedGhp := testSecrets.MustGetField("GITHUB_VERIFIED_GHP")
|
|
||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -42,32 +41,6 @@ func TestGitHub_FromChunk(t *testing.T) {
|
|||||||
want []detectors.Result
|
want []detectors.Result
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
|
||||||
name: "found, verified ghp",
|
|
||||||
s: Scanner{},
|
|
||||||
args: args{
|
|
||||||
ctx: context.Background(),
|
|
||||||
data: []byte(fmt.Sprintf("You can find a github secret %s within", verifiedGhp)),
|
|
||||||
verify: true,
|
|
||||||
},
|
|
||||||
want: []detectors.Result{
|
|
||||||
{
|
|
||||||
DetectorType: detectorspb.DetectorType_Github,
|
|
||||||
Verified: true,
|
|
||||||
ExtraData: map[string]string{
|
|
||||||
"account_type": "User",
|
|
||||||
"company": "",
|
|
||||||
"name": "",
|
|
||||||
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
||||||
"scopes": "notifications",
|
|
||||||
"site_admin": "false",
|
|
||||||
"url": "https://github.com/truffle-sandbox",
|
|
||||||
"username": "truffle-sandbox",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "found, unverified ghp",
|
name: "found, unverified ghp",
|
||||||
s: Scanner{},
|
s: Scanner{},
|
||||||
@@ -80,9 +53,6 @@ func TestGitHub_FromChunk(t *testing.T) {
|
|||||||
{
|
{
|
||||||
DetectorType: detectorspb.DetectorType_Github,
|
DetectorType: detectorspb.DetectorType_Github,
|
||||||
Verified: false,
|
Verified: false,
|
||||||
ExtraData: map[string]string{
|
|
||||||
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@@ -99,9 +69,6 @@ func TestGitHub_FromChunk(t *testing.T) {
|
|||||||
{
|
{
|
||||||
DetectorType: detectorspb.DetectorType_Github,
|
DetectorType: detectorspb.DetectorType_Github,
|
||||||
Verified: false,
|
Verified: false,
|
||||||
ExtraData: map[string]string{
|
|
||||||
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@@ -118,9 +85,6 @@ func TestGitHub_FromChunk(t *testing.T) {
|
|||||||
{
|
{
|
||||||
DetectorType: detectorspb.DetectorType_Github,
|
DetectorType: detectorspb.DetectorType_Github,
|
||||||
Verified: false,
|
Verified: false,
|
||||||
ExtraData: map[string]string{
|
|
||||||
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@@ -137,9 +101,6 @@ func TestGitHub_FromChunk(t *testing.T) {
|
|||||||
{
|
{
|
||||||
DetectorType: detectorspb.DetectorType_Github,
|
DetectorType: detectorspb.DetectorType_Github,
|
||||||
Verified: false,
|
Verified: false,
|
||||||
ExtraData: map[string]string{
|
|
||||||
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@@ -156,9 +117,6 @@ func TestGitHub_FromChunk(t *testing.T) {
|
|||||||
{
|
{
|
||||||
DetectorType: detectorspb.DetectorType_Github,
|
DetectorType: detectorspb.DetectorType_Github,
|
||||||
Verified: false,
|
Verified: false,
|
||||||
ExtraData: map[string]string{
|
|
||||||
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
@@ -175,9 +133,6 @@ func TestGitHub_FromChunk(t *testing.T) {
|
|||||||
{
|
{
|
||||||
DetectorType: detectorspb.DetectorType_Github,
|
DetectorType: detectorspb.DetectorType_Github,
|
||||||
Verified: false,
|
Verified: false,
|
||||||
ExtraData: map[string]string{
|
|
||||||
"rotation_guide": "https://howtorotate.com/docs/tutorials/github/",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ type userRes struct {
|
|||||||
SiteAdmin bool `json:"site_admin"`
|
SiteAdmin bool `json:"site_admin"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Company string `json:"company"`
|
Company string `json:"company"`
|
||||||
UserURL string `json:"html_url"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keywords are used for efficiently pre-filtering chunks.
|
// Keywords are used for efficiently pre-filtering chunks.
|
||||||
@@ -92,17 +91,6 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
res.Body.Close()
|
res.Body.Close()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
s1.Verified = true
|
s1.Verified = true
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
s1.Verified = true
|
|
||||||
s1.ExtraData["username"] = userResponse.Login
|
|
||||||
s1.ExtraData["url"] = userResponse.UserURL
|
|
||||||
s1.ExtraData["account_type"] = userResponse.Type
|
|
||||||
s1.ExtraData["site_admin"] = fmt.Sprintf("%t", userResponse.SiteAdmin)
|
|
||||||
s1.ExtraData["name"] = userResponse.Name
|
|
||||||
s1.ExtraData["company"] = userResponse.Company
|
|
||||||
s1.ExtraData["scopes"] = res.Header.Get("X-OAuth-Scopes")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const verifyURL = "https://api.pagerduty.com/users"
|
|||||||
var (
|
var (
|
||||||
defaultClient = common.SaneHttpClient()
|
defaultClient = 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{"pagerduty", "pager_duty", "pd_", "pd-"}) + `\b([a-zA-Z0-9_+-]{20})\b`)
|
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"pagerduty", "pager_duty", "pd_", "pd-"}) + `\b(u\+[a-zA-Z0-9_+-]{18})\b`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Keywords are used for efficiently pre-filtering chunks.
|
// Keywords are used for efficiently pre-filtering chunks.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package plaidkey
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -53,32 +54,36 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
DetectorType: detectorspb.DetectorType_PlaidKey,
|
DetectorType: detectorspb.DetectorType_PlaidKey,
|
||||||
Raw: []byte(resMatch),
|
Raw: []byte(resMatch),
|
||||||
}
|
}
|
||||||
|
environments := []string{"development", "production"}
|
||||||
if verify {
|
if verify {
|
||||||
payload := strings.NewReader(`{"client_id":"` + idresMatch + `","secret":"` + resMatch + `","user":{"client_user_id":"60e3ee4019a2660010f8bc54","phone_number_verified_time":"0001-01-01T00:00:00Z","email_address_verified_time":"0001-01-01T00:00:00Z"},"client_name":"Plaid Test App","products":["auth","transactions"],"country_codes":["US"],"webhook":"https://webhook-uri.com","account_filters":{"depository":{"account_subtypes":["checking","savings"]}},"language":"en","link_customization_name":"default"}`)
|
for _, env := range environments {
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", "https://development.plaid.com/link/token/create", payload)
|
payload := strings.NewReader(`{"client_id":"` + idresMatch + `","secret":"` + resMatch + `","user":{"client_user_id":"60e3ee4019a2660010f8bc54","phone_number_verified_time":"0001-01-01T00:00:00Z","email_address_verified_time":"0001-01-01T00:00:00Z"},"client_name":"Plaid Test App","products":["auth","transactions"],"country_codes":["US"],"webhook":"https://webhook-uri.com","account_filters":{"depository":{"account_subtypes":["checking","savings"]}},"language":"en","link_customization_name":"default"}`)
|
||||||
if err != nil {
|
req, err := http.NewRequestWithContext(ctx, "POST", "https://"+env+".plaid.com/link/token/create", payload)
|
||||||
continue
|
if err != nil {
|
||||||
}
|
continue
|
||||||
req.Header.Add("Content-Type", "application/json")
|
}
|
||||||
res, err := client.Do(req)
|
req.Header.Add("Content-Type", "application/json")
|
||||||
if err == nil {
|
res, err := client.Do(req)
|
||||||
defer res.Body.Close()
|
if err == nil {
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
defer res.Body.Close()
|
||||||
s1.Verified = true
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
} else {
|
s1.Verified = true
|
||||||
// This function will check false positives for common test words, but also it will make sure the key appears 'random' enough to be a real key.
|
s1.ExtraData = map[string]string{"environment": fmt.Sprintf("https://%s.plaid.com", env)}
|
||||||
if detectors.IsKnownFalsePositive(resMatch, detectors.DefaultFalsePositives, true) {
|
} else {
|
||||||
continue
|
// This function will check false positives for common test words, but also it will make sure the key appears 'random' enough to be a real key.
|
||||||
|
if detectors.IsKnownFalsePositive(resMatch, detectors.DefaultFalsePositives, true) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
results = append(results, s1)
|
||||||
|
// if the environment is dev, we don't need to check production
|
||||||
|
if s1.Verified {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, s1)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results, nil
|
return results, nil
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ func TestPlaidKey_FromChunk(t *testing.T) {
|
|||||||
secret := testSecrets.MustGetField("PLAIDKEY_SECRET")
|
secret := testSecrets.MustGetField("PLAIDKEY_SECRET")
|
||||||
inactiveSecret := testSecrets.MustGetField("PLAIDKEY_SECRET_INACTIVE")
|
inactiveSecret := testSecrets.MustGetField("PLAIDKEY_SECRET_INACTIVE")
|
||||||
id := testSecrets.MustGetField("PLAIDKEY_CLIENTID")
|
id := testSecrets.MustGetField("PLAIDKEY_CLIENTID")
|
||||||
|
// env := testSecrets.MustGetField("PLAIDKEY_ENVIRONMENT") // development or production
|
||||||
|
env := "development"
|
||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -51,6 +53,9 @@ func TestPlaidKey_FromChunk(t *testing.T) {
|
|||||||
{
|
{
|
||||||
DetectorType: detectorspb.DetectorType_PlaidKey,
|
DetectorType: detectorspb.DetectorType_PlaidKey,
|
||||||
Verified: true,
|
Verified: true,
|
||||||
|
ExtraData: map[string]string{
|
||||||
|
"environment": fmt.Sprintf("https://%s.plaid.com", env),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ 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(`\b(phc_[a-zA-Z0-9_]{43})\b`)
|
keyPat = regexp.MustCompile(`\b(phx_[a-zA-Z0-9_]{43})\b`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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{"phc_"}
|
return []string{"phx_"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromData will find and optionally verify AppPosthog secrets in a given set of bytes.
|
// FromData will find and optionally verify AppPosthog secrets in a given set of bytes.
|
||||||
@@ -47,20 +47,33 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
|||||||
}
|
}
|
||||||
|
|
||||||
if verify {
|
if verify {
|
||||||
payload := strings.NewReader(` {
|
req, err := http.NewRequestWithContext(ctx, "GET", "https://app.posthog.com/api/event/?personal_api_key="+resMatch, nil)
|
||||||
"api_key": "` + resMatch + `",
|
reqEU, errEU := http.NewRequestWithContext(ctx, "GET", "https://eu.posthog.com/api/event/?personal_api_key="+resMatch, nil)
|
||||||
"distinct_id": "1234"
|
|
||||||
}`)
|
if err != nil || errEU != nil {
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", "https://app.posthog.com/decide/", payload)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
reqEU.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
s1.Verified = true
|
s1.Verified = true
|
||||||
|
} else if res.StatusCode == 401 {
|
||||||
|
//Try EU Endpoint only if other one fails.
|
||||||
|
res, err := client.Do(reqEU)
|
||||||
|
if err == nil {
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||||
|
s1.Verified = true
|
||||||
|
} else {
|
||||||
|
if detectors.IsKnownFalsePositive(resMatch, detectors.DefaultFalsePositives, true) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// This function will check false positives for common test words, but also it will make sure the key appears 'random' enough to be a real key.
|
// This function will check false positives for common test words, but also it will make sure the key appears 'random' enough to be a real key.
|
||||||
if detectors.IsKnownFalsePositive(resMatch, detectors.DefaultFalsePositives, true) {
|
if detectors.IsKnownFalsePositive(resMatch, detectors.DefaultFalsePositives, true) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ 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{"twist"}) + `\b([0-9a-f:]{40,47})\b`)
|
accessToken = regexp.MustCompile(detectors.PrefixRegex([]string{"twist"}) + `\b(?:oauth2:)?([0-9a-f]{40})\b`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Keywords are used for efficiently pre-filtering chunks.
|
// Keywords are used for efficiently pre-filtering chunks.
|
||||||
@@ -34,7 +34,7 @@ func (s Scanner) Keywords() []string {
|
|||||||
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)
|
matches := accessToken.FindAllStringSubmatch(dataStr, -1)
|
||||||
|
|
||||||
for _, match := range matches {
|
for _, match := range matches {
|
||||||
if len(match) != 2 {
|
if len(match) != 2 {
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package voiceflow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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()
|
||||||
|
// Reference: https://developer.voiceflow.com/reference/project#dialog-manager-api-keys
|
||||||
|
//
|
||||||
|
//TODO: This includes Workspace and Legacy Workspace API keys; I haven't validated whether these actually work.
|
||||||
|
// https://github.com/voiceflow/general-runtime/blob/master/tests/runtime/lib/DataAPI/utils.unit.ts
|
||||||
|
keyPat = regexp.MustCompile(`\b(VF\.(?:(?:DM|WS)\.)?[a-fA-F0-9]{24}\.[a-zA-Z0-9]{16})\b`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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{"vf", "dm"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromData will find and optionally verify Voiceflow 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)
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
if len(match) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resMatch := strings.TrimSpace(match[1])
|
||||||
|
|
||||||
|
s1 := detectors.Result{
|
||||||
|
DetectorType: detectorspb.DetectorType_Voiceflow,
|
||||||
|
Raw: []byte(resMatch),
|
||||||
|
}
|
||||||
|
|
||||||
|
if verify {
|
||||||
|
client := s.client
|
||||||
|
if client == nil {
|
||||||
|
client = defaultClient
|
||||||
|
}
|
||||||
|
// Fetch the state for a random user.
|
||||||
|
payload := []byte(`{"question": "why is the sky blue?"}`)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", "https://general-runtime.voiceflow.com/knowledge-base/query", bytes.NewBuffer(payload))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("Authorization", resMatch)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err == nil {
|
||||||
|
if res.StatusCode == http.StatusOK {
|
||||||
|
s1.Verified = true
|
||||||
|
} else if res.StatusCode == http.StatusUnauthorized {
|
||||||
|
// The secret is determinately not verified (nothing to do)
|
||||||
|
} else {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
var bodyString string
|
||||||
|
_, err = io.Copy(&buf, res.Body)
|
||||||
|
if err == nil {
|
||||||
|
bodyString = buf.String()
|
||||||
|
}
|
||||||
|
s1.VerificationError = fmt.Errorf("unexpected HTTP response [status=%d, body=%s]", res.StatusCode, bodyString)
|
||||||
|
}
|
||||||
|
_ = res.Body.Close()
|
||||||
|
} else {
|
||||||
|
s1.VerificationError = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, s1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Scanner) Type() detectorspb.DetectorType {
|
||||||
|
return detectorspb.DetectorType_Voiceflow
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
//go:build detectors
|
||||||
|
// +build detectors
|
||||||
|
|
||||||
|
package voiceflow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||||
|
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVoiceflow_Pattern(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data string
|
||||||
|
shouldMatch bool
|
||||||
|
match string
|
||||||
|
}{
|
||||||
|
// True positives
|
||||||
|
// https://github.com/funDAOmental/endlessquest/blob/5c008f7c6a7e58c45a88b72fef4b965c258d665c/Voiceflow/agent-api/index.js#L6
|
||||||
|
{
|
||||||
|
name: `valid_result1`,
|
||||||
|
data: `// z0MG IT'S NOT A SECRET (but we'll delete it)
|
||||||
|
const API_KEY = "VF.DM.6469b4e5909a470007b96250.k4ip0SMy84jWlCsF"; // it should look like this: VF.DM.XXXXXXX.XXXXXX... keep this a secret!`,
|
||||||
|
shouldMatch: true,
|
||||||
|
match: `VF.DM.6469b4e5909a470007b96250.k4ip0SMy84jWlCsF`,
|
||||||
|
},
|
||||||
|
// https://github.com/sherifButt/ll-site/blob/b98b268214324da42a84e996e4c03c242e122680/src/components/Chatbot.jsx#L14
|
||||||
|
{
|
||||||
|
name: `valid_result2`,
|
||||||
|
data: ` const runtime = useRuntime({
|
||||||
|
verify: { authorization: 'VF.DM.652da078cde70b0008e1c5df.zsIo23VTxNXKfb9f' },
|
||||||
|
session: { userID: 'user_123' },
|
||||||
|
});`,
|
||||||
|
shouldMatch: true,
|
||||||
|
match: `VF.DM.652da078cde70b0008e1c5df.zsIo23VTxNXKfb9f`,
|
||||||
|
},
|
||||||
|
// https://github.com/the-vv/Voiceflow-chatbot/blob/324db17693dd46387ea7a020e92c4e79b94306c6/src/app/chat/chat.component.ts#L27
|
||||||
|
{
|
||||||
|
name: `valid_result3`,
|
||||||
|
data: ` this.http.delete('https://general-runtime.voiceflow.com/state/user/TEST_USER', {
|
||||||
|
headers: {
|
||||||
|
Authorization: "VF.DM.652ecc210267ec00078fc726.ZFPdEwvU0d1jiIMq"
|
||||||
|
}
|
||||||
|
}).subscribe(res => {
|
||||||
|
this.loading = false;
|
||||||
|
this.doPrompt('', { action: { type: 'launch' } });
|
||||||
|
})`,
|
||||||
|
shouldMatch: true,
|
||||||
|
match: `VF.DM.652ecc210267ec00078fc726.ZFPdEwvU0d1jiIMq`,
|
||||||
|
},
|
||||||
|
// https://github.com/legionX7/Graduation-Project-API/blob/451431771d3fba1d8c634b8855274b414d7aed6d/mainAPI.py#L547
|
||||||
|
{
|
||||||
|
name: `valid_result4`,
|
||||||
|
data: `
|
||||||
|
API_KEY = 'VF.DM.646388eb1419c80007bbbaa4.XHOqETFO3cvTxlGl'
|
||||||
|
VERSION_ID = '646bc'`,
|
||||||
|
shouldMatch: true,
|
||||||
|
match: `VF.DM.646388eb1419c80007bbbaa4.XHOqETFO3cvTxlGl`,
|
||||||
|
},
|
||||||
|
// https://github.com/voiceflow/general-runtime/blob/master/tests/runtime/lib/DataAPI/utils.unit.ts
|
||||||
|
{
|
||||||
|
name: `valid_result5`,
|
||||||
|
data: ` it('extracts ID from a Dialog Manager API key', () => {
|
||||||
|
// eslint-disable-next-line no-secrets/no-secrets
|
||||||
|
const key = 'VF.DM.628d5d92faf688001bda7907.dmC8KKO1oX8JO5ai';
|
||||||
|
const result = utils.extractAPIKeyID(key);
|
||||||
|
|
||||||
|
expect(result).to.equal('628d5d92faf688001bda7907');
|
||||||
|
});`,
|
||||||
|
shouldMatch: true,
|
||||||
|
match: `VF.DM.628d5d92faf688001bda7907.dmC8KKO1oX8JO5ai`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `valid_result6_legacy`,
|
||||||
|
data: ` it('extracts ID from a Workspace API key', () => {
|
||||||
|
// eslint-disable-next-line no-secrets/no-secrets
|
||||||
|
const key = 'VF.WS.62bcb0cca5184300066f5ac7.egnKyyzZksiS5iGa';
|
||||||
|
const result = utils.extractAPIKeyID(key);
|
||||||
|
|
||||||
|
expect(result).to.equal('62bcb0cca5184300066f5ac7');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
shouldMatch: true,
|
||||||
|
match: `VF.WS.62bcb0cca5184300066f5ac7.egnKyyzZksiS5iGa`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `valid_result7_legacy`,
|
||||||
|
data: ` it('extracts ID from a Legacy Workspace API key', () => {
|
||||||
|
// eslint-disable-next-line no-secrets/no-secrets
|
||||||
|
const key = 'VF.62bcb0cca5184300066f5ac7.dmC8KKO1oX8JO5az';
|
||||||
|
const result = utils.extractAPIKeyID(key);
|
||||||
|
|
||||||
|
expect(result).to.equal('62bcb0cca5184300066f5ac7');
|
||||||
|
});`,
|
||||||
|
shouldMatch: true,
|
||||||
|
match: `VF.62bcb0cca5184300066f5ac7.dmC8KKO1oX8JO5az`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// False positives
|
||||||
|
// https://github.com/ImperialCollegeLondon/voiceflow-integration-whatsapp/blob/0f3d6a5638b9acb4989d5bf8e77081cc78e9b976/README.md?plain=1#L155
|
||||||
|
{
|
||||||
|
name: `invalid_result1`,
|
||||||
|
data: "Now, paste it in your .env file for the **VF_PROJECT_API** variable<br>\n```VF_PROJECT_API='VF.DM.62xxxxxxxxxxxxxxxxxxxxxxx'```",
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
// https://github.com/voiceflow/api-examples/blob/c3d8ba9ee8eced7ec8d241973b1eb0284aaec212/rust/src/main.rs#L5
|
||||||
|
{
|
||||||
|
name: `invalid_result2`,
|
||||||
|
data: `const API_KEY: &str = "YOUR_API_KEY_HERE"; // it should look like this: VF.DM.XXXXXXX.XXXXXX... keep this a secret!`,
|
||||||
|
shouldMatch: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
s := Scanner{}
|
||||||
|
|
||||||
|
results, err := s.FromData(context.Background(), true, []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]
|
||||||
|
if result.VerificationError != nil {
|
||||||
|
fmt.Printf("VerificationError: %v\n", result.VerificationError)
|
||||||
|
}
|
||||||
|
resultData := string(result.Raw)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVoiceflow_FromChunk(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
defer cancel()
|
||||||
|
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not get test secrets from GCP: %s", err)
|
||||||
|
}
|
||||||
|
secret := testSecrets.MustGetField("VOICEFLOW")
|
||||||
|
inactiveSecret := testSecrets.MustGetField("VOICEFLOW_INACTIVE")
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
data []byte
|
||||||
|
verify bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
s Scanner
|
||||||
|
args args
|
||||||
|
want []detectors.Result
|
||||||
|
wantErr bool
|
||||||
|
wantVerificationErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "found, verified",
|
||||||
|
s: Scanner{},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
data: []byte(fmt.Sprintf("You can find a voiceflow secret %s within", secret)),
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
want: []detectors.Result{
|
||||||
|
{
|
||||||
|
DetectorType: detectorspb.DetectorType_Voiceflow,
|
||||||
|
Verified: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
wantVerificationErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "found, unverified",
|
||||||
|
s: Scanner{},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
data: []byte(fmt.Sprintf("You can find a voiceflow secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
want: []detectors.Result{
|
||||||
|
{
|
||||||
|
DetectorType: detectorspb.DetectorType_Voiceflow,
|
||||||
|
Verified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
wantVerificationErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not found",
|
||||||
|
s: Scanner{},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
data: []byte("You cannot find the secret within"),
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
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 voiceflow secret %s within", secret)),
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
want: []detectors.Result{
|
||||||
|
{
|
||||||
|
DetectorType: detectorspb.DetectorType_Voiceflow,
|
||||||
|
Verified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
wantVerificationErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "found, verified but unexpected api surface",
|
||||||
|
s: Scanner{client: common.ConstantResponseHttpClient(404, "")},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
data: []byte(fmt.Sprintf("You can find a voiceflow secret %s within", secret)),
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
want: []detectors.Result{
|
||||||
|
{
|
||||||
|
DetectorType: detectorspb.DetectorType_Voiceflow,
|
||||||
|
Verified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
wantVerificationErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Voiceflow.FromData() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range got {
|
||||||
|
if len(got[i].Raw) == 0 {
|
||||||
|
t.Fatalf("no raw secret present: \n %+v", got[i])
|
||||||
|
}
|
||||||
|
if (got[i].VerificationError != nil) != tt.wantVerificationErr {
|
||||||
|
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "VerificationError")
|
||||||
|
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
|
||||||
|
t.Errorf("Voiceflow.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkFromData(benchmark *testing.B) {
|
||||||
|
ctx := context.Background()
|
||||||
|
s := Scanner{}
|
||||||
|
for name, data := range detectors.MustGetBenchmarkData() {
|
||||||
|
benchmark.Run(name, func(b *testing.B) {
|
||||||
|
b.ResetTimer()
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
_, err := s.FromData(ctx, false, data)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/sourcegraphcody"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/sourcegraphcody"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/tailscale"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/tailscale"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/trufflehogenterprise"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/trufflehogenterprise"
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/voiceflow"
|
||||||
|
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/abbysale"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/abbysale"
|
||||||
@@ -1554,6 +1555,7 @@ func DefaultDetectors() []detectors.Detector {
|
|||||||
&ramp.Scanner{},
|
&ramp.Scanner{},
|
||||||
&anthropic.Scanner{},
|
&anthropic.Scanner{},
|
||||||
&sourcegraphcody.Scanner{},
|
&sourcegraphcody.Scanner{},
|
||||||
|
voiceflow.Scanner{},
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1008,6 +1008,7 @@ const (
|
|||||||
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
|
||||||
)
|
)
|
||||||
|
|
||||||
// Enum value maps for DetectorType.
|
// Enum value maps for DetectorType.
|
||||||
@@ -1946,6 +1947,7 @@ var (
|
|||||||
934: "Ramp",
|
934: "Ramp",
|
||||||
935: "Klaviyo",
|
935: "Klaviyo",
|
||||||
936: "SourcegraphCody",
|
936: "SourcegraphCody",
|
||||||
|
937: "Voiceflow",
|
||||||
}
|
}
|
||||||
DetectorType_value = map[string]int32{
|
DetectorType_value = map[string]int32{
|
||||||
"Alibaba": 0,
|
"Alibaba": 0,
|
||||||
@@ -2881,6 +2883,7 @@ var (
|
|||||||
"Ramp": 934,
|
"Ramp": 934,
|
||||||
"Klaviyo": 935,
|
"Klaviyo": 935,
|
||||||
"SourcegraphCody": 936,
|
"SourcegraphCody": 936,
|
||||||
|
"Voiceflow": 937,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3259,7 +3262,7 @@ var file_detectors_proto_rawDesc = []byte{
|
|||||||
0x44, 0x65, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55,
|
0x44, 0x65, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55,
|
||||||
0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x4c, 0x41, 0x49,
|
0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x4c, 0x41, 0x49,
|
||||||
0x4e, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x41, 0x53, 0x45, 0x36, 0x34, 0x10, 0x02, 0x12,
|
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, 0x2a, 0xa6, 0x75, 0x0a, 0x0c, 0x44,
|
0x09, 0x0a, 0x05, 0x55, 0x54, 0x46, 0x31, 0x36, 0x10, 0x03, 0x2a, 0xb6, 0x75, 0x0a, 0x0c, 0x44,
|
||||||
0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x41,
|
0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x41,
|
||||||
0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x4d, 0x51, 0x50,
|
0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x4d, 0x51, 0x50,
|
||||||
0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x57, 0x53, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x41,
|
0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x57, 0x53, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x41,
|
||||||
@@ -4198,7 +4201,8 @@ var file_detectors_proto_rawDesc = []byte{
|
|||||||
0x63, 0x10, 0xa5, 0x07, 0x12, 0x09, 0x0a, 0x04, 0x52, 0x61, 0x6d, 0x70, 0x10, 0xa6, 0x07, 0x12,
|
0x63, 0x10, 0xa5, 0x07, 0x12, 0x09, 0x0a, 0x04, 0x52, 0x61, 0x6d, 0x70, 0x10, 0xa6, 0x07, 0x12,
|
||||||
0x0c, 0x0a, 0x07, 0x4b, 0x6c, 0x61, 0x76, 0x69, 0x79, 0x6f, 0x10, 0xa7, 0x07, 0x12, 0x14, 0x0a,
|
0x0c, 0x0a, 0x07, 0x4b, 0x6c, 0x61, 0x76, 0x69, 0x79, 0x6f, 0x10, 0xa7, 0x07, 0x12, 0x14, 0x0a,
|
||||||
0x0f, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x67, 0x72, 0x61, 0x70, 0x68, 0x43, 0x6f, 0x64, 0x79,
|
0x0f, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x67, 0x72, 0x61, 0x70, 0x68, 0x43, 0x6f, 0x64, 0x79,
|
||||||
0x10, 0xa8, 0x07, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
|
0x10, 0xa8, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x56, 0x6f, 0x69, 0x63, 0x65, 0x66, 0x6c, 0x6f, 0x77,
|
||||||
|
0x10, 0xa9, 0x07, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
|
||||||
0x6d, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74,
|
0x6d, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74,
|
||||||
0x79, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f, 0x67, 0x2f, 0x76, 0x33, 0x2f,
|
0x79, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f, 0x67, 0x2f, 0x76, 0x33, 0x2f,
|
||||||
0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73,
|
0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73,
|
||||||
|
|||||||
@@ -825,38 +825,51 @@ func (s *Source) scan(ctx context.Context, installationClient *github.Client, ch
|
|||||||
// Unauthenticated access to most github endpoints has a rate limit of 60 requests per hour.
|
// Unauthenticated access to most github endpoints has a rate limit of 60 requests per hour.
|
||||||
// This will likely only be exhausted if many users/orgs are scanned without auth
|
// This will likely only be exhausted if many users/orgs are scanned without auth
|
||||||
func (s *Source) handleRateLimit(errIn error, res *github.Response) bool {
|
func (s *Source) handleRateLimit(errIn error, res *github.Response) bool {
|
||||||
limit, ok := errIn.(*github.RateLimitError)
|
var (
|
||||||
if !ok {
|
knownWait = true
|
||||||
|
remaining = 0
|
||||||
|
retryAfter time.Duration
|
||||||
|
)
|
||||||
|
|
||||||
|
// GitHub has both primary (RateLimit) and secondary (AbuseRateLimit) errors.
|
||||||
|
var rateLimit *github.RateLimitError
|
||||||
|
var abuseLimit *github.AbuseRateLimitError
|
||||||
|
if errors.As(errIn, &rateLimit) {
|
||||||
|
// Do nothing
|
||||||
|
} else if errors.As(errIn, &abuseLimit) {
|
||||||
|
retryAfter = abuseLimit.GetRetryAfter()
|
||||||
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
githubNumRateLimitEncountered.WithLabelValues(s.name).Inc()
|
githubNumRateLimitEncountered.WithLabelValues(s.name).Inc()
|
||||||
|
// Parse retry information from response headers, unless a Retry-After value was already provided.
|
||||||
if res != nil {
|
// https://docs.github.com/en/rest/overview/resources-in-the-rest-api#exceeding-the-rate-limit
|
||||||
knownWait := true
|
if retryAfter <= 0 && res != nil {
|
||||||
remaining, err := strconv.Atoi(res.Header.Get("x-ratelimit-remaining"))
|
var err error
|
||||||
|
remaining, err = strconv.Atoi(res.Header.Get("x-ratelimit-remaining"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
knownWait = false
|
knownWait = false
|
||||||
}
|
}
|
||||||
|
|
||||||
resetTime, err := strconv.Atoi(res.Header.Get("x-ratelimit-reset"))
|
resetTime, err := strconv.Atoi(res.Header.Get("x-ratelimit-reset"))
|
||||||
if err != nil || resetTime == 0 {
|
if err != nil || resetTime == 0 {
|
||||||
knownWait = false
|
knownWait = false
|
||||||
}
|
} else if resetTime > 0 {
|
||||||
|
retryAfter = time.Duration(int64(resetTime)-time.Now().Unix()) * time.Second
|
||||||
if knownWait && remaining == 0 {
|
|
||||||
waitTime := int64(resetTime) - time.Now().Unix()
|
|
||||||
if waitTime > 0 {
|
|
||||||
duration := time.Duration(waitTime+1) * time.Second
|
|
||||||
s.log.V(2).Info("rate limited", "resumeTime", time.Now().Add(duration).String())
|
|
||||||
time.Sleep(duration)
|
|
||||||
githubSecondsSpentRateLimited.WithLabelValues(s.name).Add(duration.Seconds())
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log.V(2).Info("handling rate limit (5 minutes retry)", "retry-after", limit.Message)
|
resumeTime := time.Now().Add(retryAfter).String()
|
||||||
time.Sleep(time.Minute * 5)
|
if knownWait && remaining == 0 && retryAfter > 0 {
|
||||||
|
s.log.V(2).Info("rate limited", "retry_after", retryAfter.String(), "resume_time", resumeTime)
|
||||||
|
} else {
|
||||||
|
// TODO: Use exponential backoff instead of static retry time.
|
||||||
|
retryAfter = time.Minute * 5
|
||||||
|
s.log.V(2).Error(errIn, "unexpected rate limit error", "retry_after", retryAfter.String(), "resume_time", resumeTime)
|
||||||
|
}
|
||||||
|
time.Sleep(retryAfter)
|
||||||
|
githubSecondsSpentRateLimited.WithLabelValues(s.name).Add(retryAfter.Seconds())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1131,9 +1144,10 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type repoInfo struct {
|
type repoInfo struct {
|
||||||
owner string
|
owner string
|
||||||
repo string
|
repo string
|
||||||
repoPath string
|
repoPath string
|
||||||
|
visibility source_metadatapb.Visibility
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Source) processRepoComments(ctx context.Context, repoPath string, trimmedURL []string, repoURL *url.URL, chunksChan chan *sources.Chunk) error {
|
func (s *Source) processRepoComments(ctx context.Context, repoPath string, trimmedURL []string, repoURL *url.URL, chunksChan chan *sources.Chunk) error {
|
||||||
@@ -1144,7 +1158,16 @@ func (s *Source) processRepoComments(ctx context.Context, repoPath string, trimm
|
|||||||
owner := trimmedURL[1]
|
owner := trimmedURL[1]
|
||||||
repo := trimmedURL[2]
|
repo := trimmedURL[2]
|
||||||
|
|
||||||
repoInfo := repoInfo{owner: owner, repo: repo, repoPath: repoPath}
|
if !(s.includeIssueComments || s.includePRComments) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
repoInfo := repoInfo{
|
||||||
|
owner: owner,
|
||||||
|
repo: repo,
|
||||||
|
repoPath: repoPath,
|
||||||
|
visibility: s.visibilityOf(ctx, repoPath),
|
||||||
|
}
|
||||||
|
|
||||||
if s.includeIssueComments {
|
if s.includeIssueComments {
|
||||||
if err := s.processIssueComments(ctx, repoInfo, chunksChan); err != nil {
|
if err := s.processIssueComments(ctx, repoInfo, chunksChan); err != nil {
|
||||||
@@ -1191,7 +1214,7 @@ func (s *Source) processIssues(ctx context.Context, info repoInfo, chunksChan ch
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = s.chunkIssues(ctx, info.repo, info.repoPath, issues, chunksChan); err != nil {
|
if err = s.chunkIssues(ctx, info, issues, chunksChan); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1226,7 +1249,7 @@ func (s *Source) processIssueComments(ctx context.Context, info repoInfo, chunks
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = s.chunkIssueComments(ctx, info.repo, info.repoPath, issueComments, chunksChan); err != nil {
|
if err = s.chunkIssueComments(ctx, info, issueComments, chunksChan); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1262,7 +1285,7 @@ func (s *Source) processPRs(ctx context.Context, info repoInfo, chunksChan chan
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = s.chunkPullRequests(ctx, info.repo, prs, chunksChan); err != nil {
|
if err = s.chunkPullRequests(ctx, info, prs, chunksChan); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1297,7 +1320,7 @@ func (s *Source) processPRComments(ctx context.Context, info repoInfo, chunksCha
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = s.chunkPullRequestComments(ctx, info.repo, prComments, chunksChan); err != nil {
|
if err = s.chunkPullRequestComments(ctx, info, prComments, chunksChan); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1310,7 +1333,7 @@ func (s *Source) processPRComments(ctx context.Context, info repoInfo, chunksCha
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Source) chunkIssues(ctx context.Context, repo, repoPath string, issues []*github.Issue, chunksChan chan *sources.Chunk) error {
|
func (s *Source) chunkIssues(ctx context.Context, repoInfo repoInfo, issues []*github.Issue, chunksChan chan *sources.Chunk) error {
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
|
|
||||||
// Skip pull requests since covered by processPRs.
|
// Skip pull requests since covered by processPRs.
|
||||||
@@ -1330,9 +1353,9 @@ func (s *Source) chunkIssues(ctx context.Context, repo, repoPath string, issues
|
|||||||
Link: sanitizer.UTF8(issue.GetHTMLURL()),
|
Link: sanitizer.UTF8(issue.GetHTMLURL()),
|
||||||
Username: sanitizer.UTF8(issue.GetUser().GetLogin()),
|
Username: sanitizer.UTF8(issue.GetUser().GetLogin()),
|
||||||
Email: sanitizer.UTF8(issue.GetUser().GetEmail()),
|
Email: sanitizer.UTF8(issue.GetUser().GetEmail()),
|
||||||
Repository: sanitizer.UTF8(repo),
|
Repository: sanitizer.UTF8(repoInfo.repo),
|
||||||
Timestamp: sanitizer.UTF8(issue.GetCreatedAt().String()),
|
Timestamp: sanitizer.UTF8(issue.GetCreatedAt().String()),
|
||||||
Visibility: s.visibilityOf(ctx, repoPath),
|
Visibility: repoInfo.visibility,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1349,7 +1372,7 @@ func (s *Source) chunkIssues(ctx context.Context, repo, repoPath string, issues
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Source) chunkIssueComments(ctx context.Context, repo, repoPath string, comments []*github.IssueComment, chunksChan chan *sources.Chunk) error {
|
func (s *Source) chunkIssueComments(ctx context.Context, repoInfo repoInfo, comments []*github.IssueComment, chunksChan chan *sources.Chunk) error {
|
||||||
for _, comment := range comments {
|
for _, comment := range comments {
|
||||||
// Create chunk and send it to the channel.
|
// Create chunk and send it to the channel.
|
||||||
chunk := &sources.Chunk{
|
chunk := &sources.Chunk{
|
||||||
@@ -1363,9 +1386,9 @@ func (s *Source) chunkIssueComments(ctx context.Context, repo, repoPath string,
|
|||||||
Link: sanitizer.UTF8(comment.GetHTMLURL()),
|
Link: sanitizer.UTF8(comment.GetHTMLURL()),
|
||||||
Username: sanitizer.UTF8(comment.GetUser().GetLogin()),
|
Username: sanitizer.UTF8(comment.GetUser().GetLogin()),
|
||||||
Email: sanitizer.UTF8(comment.GetUser().GetEmail()),
|
Email: sanitizer.UTF8(comment.GetUser().GetEmail()),
|
||||||
Repository: sanitizer.UTF8(repo),
|
Repository: sanitizer.UTF8(repoInfo.repo),
|
||||||
Timestamp: sanitizer.UTF8(comment.GetCreatedAt().String()),
|
Timestamp: sanitizer.UTF8(comment.GetCreatedAt().String()),
|
||||||
Visibility: s.visibilityOf(ctx, repoPath),
|
Visibility: repoInfo.visibility,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1382,7 +1405,7 @@ func (s *Source) chunkIssueComments(ctx context.Context, repo, repoPath string,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Source) chunkPullRequestComments(ctx context.Context, repo string, comments []*github.PullRequestComment, chunksChan chan *sources.Chunk) error {
|
func (s *Source) chunkPullRequestComments(ctx context.Context, repoInfo repoInfo, comments []*github.PullRequestComment, chunksChan chan *sources.Chunk) error {
|
||||||
for _, comment := range comments {
|
for _, comment := range comments {
|
||||||
// Create chunk and send it to the channel.
|
// Create chunk and send it to the channel.
|
||||||
chunk := &sources.Chunk{
|
chunk := &sources.Chunk{
|
||||||
@@ -1396,8 +1419,9 @@ func (s *Source) chunkPullRequestComments(ctx context.Context, repo string, comm
|
|||||||
Link: sanitizer.UTF8(comment.GetHTMLURL()),
|
Link: sanitizer.UTF8(comment.GetHTMLURL()),
|
||||||
Username: sanitizer.UTF8(comment.GetUser().GetLogin()),
|
Username: sanitizer.UTF8(comment.GetUser().GetLogin()),
|
||||||
Email: sanitizer.UTF8(comment.GetUser().GetEmail()),
|
Email: sanitizer.UTF8(comment.GetUser().GetEmail()),
|
||||||
Repository: sanitizer.UTF8(repo),
|
Repository: sanitizer.UTF8(repoInfo.repo),
|
||||||
Timestamp: sanitizer.UTF8(comment.GetCreatedAt().String()),
|
Timestamp: sanitizer.UTF8(comment.GetCreatedAt().String()),
|
||||||
|
Visibility: repoInfo.visibility,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1414,7 +1438,7 @@ func (s *Source) chunkPullRequestComments(ctx context.Context, repo string, comm
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Source) chunkPullRequests(ctx context.Context, repo string, prs []*github.PullRequest, chunksChan chan *sources.Chunk) error {
|
func (s *Source) chunkPullRequests(ctx context.Context, repoInfo repoInfo, prs []*github.PullRequest, chunksChan chan *sources.Chunk) error {
|
||||||
for _, pr := range prs {
|
for _, pr := range prs {
|
||||||
// Create chunk and send it to the channel.
|
// Create chunk and send it to the channel.
|
||||||
chunk := &sources.Chunk{
|
chunk := &sources.Chunk{
|
||||||
@@ -1428,8 +1452,9 @@ func (s *Source) chunkPullRequests(ctx context.Context, repo string, prs []*gith
|
|||||||
Link: sanitizer.UTF8(pr.GetHTMLURL()),
|
Link: sanitizer.UTF8(pr.GetHTMLURL()),
|
||||||
Username: sanitizer.UTF8(pr.GetUser().GetLogin()),
|
Username: sanitizer.UTF8(pr.GetUser().GetLogin()),
|
||||||
Email: sanitizer.UTF8(pr.GetUser().GetEmail()),
|
Email: sanitizer.UTF8(pr.GetUser().GetEmail()),
|
||||||
Repository: sanitizer.UTF8(repo),
|
Repository: sanitizer.UTF8(repoInfo.repo),
|
||||||
Timestamp: sanitizer.UTF8(pr.GetCreatedAt().String()),
|
Timestamp: sanitizer.UTF8(pr.GetCreatedAt().String()),
|
||||||
|
Visibility: repoInfo.visibility,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1462,8 +1487,7 @@ func (s *Source) chunkGistComments(ctx context.Context, gistUrl string, comments
|
|||||||
Email: sanitizer.UTF8(comment.GetUser().GetEmail()),
|
Email: sanitizer.UTF8(comment.GetUser().GetEmail()),
|
||||||
Repository: sanitizer.UTF8(gistUrl),
|
Repository: sanitizer.UTF8(gistUrl),
|
||||||
Timestamp: sanitizer.UTF8(comment.GetCreatedAt().String()),
|
Timestamp: sanitizer.UTF8(comment.GetCreatedAt().String()),
|
||||||
// Fetching this information requires making an additional API call.
|
// TODO: Fetching this requires making an additional API call. We may want to include this in the future.
|
||||||
// We may want to include this in the future.
|
|
||||||
// Visibility: s.visibilityOf(ctx, repoPath),
|
// Visibility: s.visibilityOf(ctx, repoPath),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -89,14 +89,14 @@ func (f Fatal) Unwrap() error { return f.error }
|
|||||||
// ChunkError is a custom error type for errors encountered during chunking of
|
// ChunkError is a custom error type for errors encountered during chunking of
|
||||||
// a specific unit.
|
// a specific unit.
|
||||||
type ChunkError struct {
|
type ChunkError struct {
|
||||||
unit SourceUnit
|
Unit SourceUnit
|
||||||
err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f ChunkError) Error() string {
|
func (f ChunkError) Error() string {
|
||||||
return fmt.Sprintf("error chunking unit %q: %s", f.unit.SourceUnitID(), f.err.Error())
|
return fmt.Sprintf("error chunking unit %q: %s", f.Unit.SourceUnitID(), f.Err.Error())
|
||||||
}
|
}
|
||||||
func (f ChunkError) Unwrap() error { return f.err }
|
func (f ChunkError) Unwrap() error { return f.Err }
|
||||||
|
|
||||||
// JobProgress aggregates information about a run of a Source.
|
// JobProgress aggregates information about a run of a Source.
|
||||||
type JobProgress struct {
|
type JobProgress struct {
|
||||||
@@ -351,3 +351,17 @@ func (m JobProgressMetrics) ElapsedTime() time.Duration {
|
|||||||
}
|
}
|
||||||
return m.EndTime.Sub(m.StartTime)
|
return m.EndTime.Sub(m.StartTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrorsFor returns all the errors for the given SourceUnit. If there are no
|
||||||
|
// errors, including the case that the unit has not been encountered, nil will
|
||||||
|
// be returned.
|
||||||
|
func (m JobProgressMetrics) ErrorsFor(unit SourceUnit) []error {
|
||||||
|
var errs []error
|
||||||
|
for _, err := range m.Errors {
|
||||||
|
var chunkErr ChunkError
|
||||||
|
if errors.As(err, &chunkErr) && chunkErr.Unit == unit {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
package sources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
lru "github.com/hashicorp/golang-lru/v2"
|
||||||
|
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnitHook implements JobProgressHook for tracking the progress of each
|
||||||
|
// individual unit.
|
||||||
|
type UnitHook struct {
|
||||||
|
metrics *lru.Cache[string, *UnitMetrics]
|
||||||
|
mu sync.Mutex
|
||||||
|
NoopHook
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnitHookOpt func(*UnitHook)
|
||||||
|
|
||||||
|
func WithUnitHookCache(cache *lru.Cache[string, *UnitMetrics]) UnitHookOpt {
|
||||||
|
return func(hook *UnitHook) { hook.metrics = cache }
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUnitHook(ctx context.Context, opts ...UnitHookOpt) *UnitHook {
|
||||||
|
// lru.NewWithEvict can only fail if the size is < 0.
|
||||||
|
cache, _ := lru.NewWithEvict(1024, func(key string, value *UnitMetrics) {
|
||||||
|
if value.handled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Logger().Error(fmt.Errorf("eviction"), "dropping unit metric",
|
||||||
|
"id", key,
|
||||||
|
"metric", value,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
hook := UnitHook{metrics: cache}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&hook)
|
||||||
|
}
|
||||||
|
return &hook
|
||||||
|
}
|
||||||
|
|
||||||
|
// id is a helper method to generate an ID for the given job and unit.
|
||||||
|
func (u *UnitHook) id(ref JobProgressRef, unit SourceUnit) string {
|
||||||
|
unitID := ""
|
||||||
|
if unit != nil {
|
||||||
|
unitID = unit.SourceUnitID()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d/%d/%s", ref.SourceID, ref.JobID, unitID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UnitHook) StartUnitChunking(ref JobProgressRef, unit SourceUnit, start time.Time) {
|
||||||
|
id := u.id(ref, unit)
|
||||||
|
u.mu.Lock()
|
||||||
|
defer u.mu.Unlock()
|
||||||
|
|
||||||
|
u.metrics.Add(id, &UnitMetrics{
|
||||||
|
Unit: unit,
|
||||||
|
Parent: ref,
|
||||||
|
StartTime: start,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UnitHook) EndUnitChunking(ref JobProgressRef, unit SourceUnit, end time.Time) {
|
||||||
|
id := u.id(ref, unit)
|
||||||
|
u.mu.Lock()
|
||||||
|
defer u.mu.Unlock()
|
||||||
|
|
||||||
|
metrics, ok := u.metrics.Get(id)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
metrics.EndTime = end
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UnitHook) ReportChunk(ref JobProgressRef, unit SourceUnit, chunk *Chunk) {
|
||||||
|
id := u.id(ref, unit)
|
||||||
|
u.mu.Lock()
|
||||||
|
defer u.mu.Unlock()
|
||||||
|
|
||||||
|
metrics, ok := u.metrics.Get(id)
|
||||||
|
if !ok && unit != nil {
|
||||||
|
// The unit has been evicted.
|
||||||
|
return
|
||||||
|
} else if !ok && unit == nil {
|
||||||
|
// This is a chunk from a non-unit source.
|
||||||
|
metrics = &UnitMetrics{
|
||||||
|
Unit: nil,
|
||||||
|
Parent: ref,
|
||||||
|
StartTime: ref.Snapshot().StartTime,
|
||||||
|
}
|
||||||
|
u.metrics.Add(id, metrics)
|
||||||
|
}
|
||||||
|
metrics.TotalChunks++
|
||||||
|
metrics.TotalBytes += uint64(len(chunk.Data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UnitHook) ReportError(ref JobProgressRef, err error) {
|
||||||
|
u.mu.Lock()
|
||||||
|
defer u.mu.Unlock()
|
||||||
|
|
||||||
|
// Always add the error to the nil unit if it exists.
|
||||||
|
if metrics, ok := u.metrics.Get(u.id(ref, nil)); ok {
|
||||||
|
metrics.Errors = append(metrics.Errors, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a ChunkError for a specific unit.
|
||||||
|
var chunkErr ChunkError
|
||||||
|
if !errors.As(err, &chunkErr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := u.id(ref, chunkErr.Unit)
|
||||||
|
|
||||||
|
metrics, ok := u.metrics.Get(id)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
metrics.Errors = append(metrics.Errors, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UnitHook) Finish(ref JobProgressRef) {
|
||||||
|
u.mu.Lock()
|
||||||
|
defer u.mu.Unlock()
|
||||||
|
// Clear out any metrics on this job. This covers the case for the
|
||||||
|
// source running without unit support.
|
||||||
|
prefix := u.id(ref, nil)
|
||||||
|
for _, id := range u.metrics.Keys() {
|
||||||
|
if !strings.HasPrefix(id, prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
metric, ok := u.metrics.Get(id)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If the unit is nil, the source does not support units.
|
||||||
|
// Use the overall job metrics instead.
|
||||||
|
if metric.Unit == nil {
|
||||||
|
snap := ref.Snapshot()
|
||||||
|
metric.StartTime = snap.StartTime
|
||||||
|
metric.EndTime = snap.EndTime
|
||||||
|
metric.Errors = snap.Errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnitMetrics gets all the currently active or newly finished metrics for this
|
||||||
|
// job. If a unit returned from this method has finished, it will be removed
|
||||||
|
// from the cache and no longer returned in successive calls to UnitMetrics().
|
||||||
|
func (u *UnitHook) UnitMetrics() []UnitMetrics {
|
||||||
|
u.mu.Lock()
|
||||||
|
defer u.mu.Unlock()
|
||||||
|
output := make([]UnitMetrics, 0, u.metrics.Len())
|
||||||
|
for _, id := range u.metrics.Keys() {
|
||||||
|
metric, ok := u.metrics.Get(id)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
output = append(output, *metric)
|
||||||
|
if metric.IsFinished() {
|
||||||
|
metric.handled = true
|
||||||
|
u.metrics.Remove(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnitMetrics struct {
|
||||||
|
Unit SourceUnit
|
||||||
|
Parent JobProgressRef
|
||||||
|
// Start and end time for chunking this unit.
|
||||||
|
StartTime time.Time
|
||||||
|
EndTime time.Time
|
||||||
|
// Total number of chunks produced from this unit.
|
||||||
|
TotalChunks uint64
|
||||||
|
// Total number of bytes produced from this unit.
|
||||||
|
TotalBytes uint64
|
||||||
|
// All errors encountered by this unit.
|
||||||
|
Errors []error
|
||||||
|
// Flag to mark that these metrics were intentionally evicted from
|
||||||
|
// the cache.
|
||||||
|
handled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UnitMetrics) IsFinished() bool {
|
||||||
|
return !u.EndTime.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ElapsedTime is a convenience method that provides the elapsed time the job
|
||||||
|
// has been running. If it hasn't started yet, 0 is returned. If it has
|
||||||
|
// finished, the total time is returned.
|
||||||
|
func (u UnitMetrics) ElapsedTime() time.Duration {
|
||||||
|
if u.StartTime.IsZero() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if u.EndTime.IsZero() {
|
||||||
|
return time.Since(u.StartTime)
|
||||||
|
}
|
||||||
|
return u.EndTime.Sub(u.StartTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoopHook implements JobProgressHook by doing nothing. This is useful for
|
||||||
|
// embedding in other structs to overwrite only the methods of the interface
|
||||||
|
// that you care about.
|
||||||
|
type NoopHook struct{}
|
||||||
|
|
||||||
|
func (NoopHook) Start(JobProgressRef, time.Time) {}
|
||||||
|
func (NoopHook) End(JobProgressRef, time.Time) {}
|
||||||
|
func (NoopHook) StartEnumerating(JobProgressRef, time.Time) {}
|
||||||
|
func (NoopHook) EndEnumerating(JobProgressRef, time.Time) {}
|
||||||
|
func (NoopHook) StartUnitChunking(JobProgressRef, SourceUnit, time.Time) {}
|
||||||
|
func (NoopHook) EndUnitChunking(JobProgressRef, SourceUnit, time.Time) {}
|
||||||
|
func (NoopHook) ReportError(JobProgressRef, error) {}
|
||||||
|
func (NoopHook) ReportUnit(JobProgressRef, SourceUnit) {}
|
||||||
|
func (NoopHook) ReportChunk(JobProgressRef, SourceUnit, *Chunk) {}
|
||||||
|
func (NoopHook) Finish(JobProgressRef) {}
|
||||||
@@ -126,3 +126,26 @@ func TestJobProgressElapsedTime(t *testing.T) {
|
|||||||
metrics.EndTime = metrics.StartTime.Add(1 * time.Hour)
|
metrics.EndTime = metrics.StartTime.Add(1 * time.Hour)
|
||||||
assert.Equal(t, metrics.ElapsedTime(), 1*time.Hour)
|
assert.Equal(t, metrics.ElapsedTime(), 1*time.Hour)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestJobProgressErrorsFor(t *testing.T) {
|
||||||
|
metrics := JobProgressMetrics{
|
||||||
|
Errors: []error{
|
||||||
|
Fatal{ChunkError{
|
||||||
|
Unit: CommonSourceUnit{ID: "foo"},
|
||||||
|
Err: fmt.Errorf("foo error"),
|
||||||
|
}},
|
||||||
|
ChunkError{
|
||||||
|
Unit: CommonSourceUnit{ID: "foo"},
|
||||||
|
Err: fmt.Errorf("foo again error"),
|
||||||
|
},
|
||||||
|
ChunkError{
|
||||||
|
Unit: CommonSourceUnit{ID: "bar"},
|
||||||
|
Err: fmt.Errorf("bar error"),
|
||||||
|
},
|
||||||
|
fmt.Errorf("hi there"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, 2, len(metrics.ErrorsFor(CommonSourceUnit{ID: "foo"})))
|
||||||
|
assert.Equal(t, 1, len(metrics.ErrorsFor(CommonSourceUnit{ID: "bar"})))
|
||||||
|
assert.Equal(t, 0, len(metrics.ErrorsFor(CommonSourceUnit{ID: "baz"})))
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package sources
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
lru "github.com/hashicorp/golang-lru/v2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"google.golang.org/protobuf/types/known/anypb"
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
|
|
||||||
@@ -200,6 +202,8 @@ func (c *unitChunker) ChunkUnit(ctx context.Context, unit SourceUnit, rep ChunkR
|
|||||||
if err := rep.ChunkErr(ctx, fmt.Errorf(step.err)); err != nil {
|
if err := rep.ChunkErr(ctx, fmt.Errorf(step.err)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if step.output == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := rep.ChunkOk(ctx, Chunk{Data: []byte(step.output)}); err != nil {
|
if err := rep.ChunkOk(ctx, Chunk{Data: []byte(step.output)}); err != nil {
|
||||||
@@ -310,3 +314,114 @@ func TestSourceManagerAvailableCapacity(t *testing.T) {
|
|||||||
<-ref.Done() // Wait for the job to finish.
|
<-ref.Done() // Wait for the job to finish.
|
||||||
assert.Equal(t, 1337, mgr.AvailableCapacity())
|
assert.Equal(t, 1337, mgr.AvailableCapacity())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSourceManagerUnitHook(t *testing.T) {
|
||||||
|
hook := NewUnitHook(context.TODO())
|
||||||
|
|
||||||
|
input := []unitChunk{
|
||||||
|
{unit: "one", output: "bar"},
|
||||||
|
{unit: "two", err: "oh no"},
|
||||||
|
{unit: "three", err: "not again"},
|
||||||
|
}
|
||||||
|
mgr := NewManager(
|
||||||
|
WithBufferedOutput(8),
|
||||||
|
WithSourceUnits(), WithConcurrentUnits(1),
|
||||||
|
WithReportHook(hook),
|
||||||
|
)
|
||||||
|
source, err := buildDummy(&unitChunker{input})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
ref, err := mgr.Run(context.Background(), "dummy", source)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
<-ref.Done()
|
||||||
|
|
||||||
|
metrics := hook.UnitMetrics()
|
||||||
|
assert.Equal(t, 3, len(metrics))
|
||||||
|
sort.Slice(metrics, func(i, j int) bool {
|
||||||
|
return metrics[i].EndTime.Before(metrics[j].EndTime)
|
||||||
|
})
|
||||||
|
m0, m1, m2 := metrics[0], metrics[1], metrics[2]
|
||||||
|
|
||||||
|
assert.Equal(t, "one", m0.Unit.SourceUnitID())
|
||||||
|
assert.Equal(t, uint64(1), m0.TotalChunks)
|
||||||
|
assert.Equal(t, uint64(3), m0.TotalBytes)
|
||||||
|
assert.NotZero(t, m0.StartTime)
|
||||||
|
assert.NotZero(t, m0.EndTime)
|
||||||
|
assert.NotZero(t, m0.ElapsedTime())
|
||||||
|
assert.Equal(t, 0, len(m0.Errors))
|
||||||
|
|
||||||
|
assert.Equal(t, "two", m1.Unit.SourceUnitID())
|
||||||
|
assert.Equal(t, uint64(0), m1.TotalChunks)
|
||||||
|
assert.Equal(t, uint64(0), m1.TotalBytes)
|
||||||
|
assert.NotZero(t, m1.StartTime)
|
||||||
|
assert.NotZero(t, m1.EndTime)
|
||||||
|
assert.NotZero(t, m1.ElapsedTime())
|
||||||
|
assert.Equal(t, 1, len(m1.Errors))
|
||||||
|
|
||||||
|
assert.Equal(t, "three", m2.Unit.SourceUnitID())
|
||||||
|
assert.Equal(t, uint64(0), m2.TotalChunks)
|
||||||
|
assert.Equal(t, uint64(0), m2.TotalBytes)
|
||||||
|
assert.NotZero(t, m2.StartTime)
|
||||||
|
assert.NotZero(t, m2.EndTime)
|
||||||
|
assert.NotZero(t, m2.ElapsedTime())
|
||||||
|
assert.Equal(t, 1, len(m2.Errors))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSourceManagerUnitHookNoBlock tests that the UnitHook drops metrics if
|
||||||
|
// they aren't handled fast enough.
|
||||||
|
func TestSourceManagerUnitHookNoBlock(t *testing.T) {
|
||||||
|
var evictedKeys []string
|
||||||
|
cache, _ := lru.NewWithEvict(1, func(key string, _ *UnitMetrics) {
|
||||||
|
evictedKeys = append(evictedKeys, key)
|
||||||
|
})
|
||||||
|
hook := NewUnitHook(context.TODO(), WithUnitHookCache(cache))
|
||||||
|
|
||||||
|
input := []unitChunk{
|
||||||
|
{unit: "one", output: "bar"},
|
||||||
|
{unit: "two", err: "oh no"},
|
||||||
|
{unit: "three", err: "not again"},
|
||||||
|
}
|
||||||
|
mgr := NewManager(
|
||||||
|
WithBufferedOutput(8),
|
||||||
|
WithSourceUnits(), WithConcurrentUnits(1),
|
||||||
|
WithReportHook(hook),
|
||||||
|
)
|
||||||
|
source, err := buildDummy(&unitChunker{input})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
ref, err := mgr.Run(context.Background(), "dummy", source)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
<-ref.Done()
|
||||||
|
|
||||||
|
assert.Equal(t, 2, len(evictedKeys))
|
||||||
|
metrics := hook.UnitMetrics()
|
||||||
|
assert.Equal(t, 1, len(metrics))
|
||||||
|
assert.Equal(t, "three", metrics[0].Unit.SourceUnitID())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSourceManagerUnitHookNoUnits tests whether the UnitHook works for
|
||||||
|
// sources that don't support units.
|
||||||
|
func TestSourceManagerUnitHookNoUnits(t *testing.T) {
|
||||||
|
hook := NewUnitHook(context.TODO())
|
||||||
|
|
||||||
|
mgr := NewManager(
|
||||||
|
WithBufferedOutput(8),
|
||||||
|
WithReportHook(hook),
|
||||||
|
)
|
||||||
|
source, err := buildDummy(&counterChunker{count: 5})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
ref, err := mgr.Run(context.Background(), "dummy", source)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
<-ref.Done()
|
||||||
|
|
||||||
|
metrics := hook.UnitMetrics()
|
||||||
|
assert.Equal(t, 1, len(metrics))
|
||||||
|
|
||||||
|
m := metrics[0]
|
||||||
|
assert.Equal(t, nil, m.Unit)
|
||||||
|
assert.Equal(t, uint64(5), m.TotalChunks)
|
||||||
|
assert.Equal(t, uint64(5), m.TotalBytes)
|
||||||
|
assert.NotZero(t, m.StartTime)
|
||||||
|
assert.NotZero(t, m.EndTime)
|
||||||
|
assert.NotZero(t, m.ElapsedTime())
|
||||||
|
assert.Equal(t, 0, len(m.Errors))
|
||||||
|
}
|
||||||
|
|||||||
@@ -945,6 +945,7 @@ enum DetectorType {
|
|||||||
Ramp = 934;
|
Ramp = 934;
|
||||||
Klaviyo = 935;
|
Klaviyo = 935;
|
||||||
SourcegraphCody = 936;
|
SourcegraphCody = 936;
|
||||||
|
Voiceflow = 937;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Result {
|
message Result {
|
||||||
|
|||||||
Reference in New Issue
Block a user