Skip to content

Commit ed6d045

Browse files
fix: fixed verification endpoint and verification logic for brand fetch (#3470)
* fix: fixed verification endpoint and verification logic * feat: introduced a new detector for apis Signed-off-by: Sahil Silare <[email protected]> * feat: added versioner impl Signed-off-by: Sahil Silare <[email protected]> * refactor: added abstraction Signed-off-by: Sahil Silare <[email protected]> * linter issues fixed * fixed all issues in v1 * brandfetch v2 detector added with pattern tests * fixed brandfetch v2 integration tests * updated the pattern test cases --------- Signed-off-by: Sahil Silare <[email protected]> Co-authored-by: Shahzad Haider <[email protected]> Co-authored-by: Shahzad Haider <[email protected]>
1 parent c53f4d3 commit ed6d045

File tree

7 files changed

+397
-38
lines changed

7 files changed

+397
-38
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package brandfetch
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"strconv"
7+
8+
regexp "github.com/wasilibs/go-re2"
9+
10+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
12+
v2 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/brandfetch/v2"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
14+
)
15+
16+
type Scanner struct {
17+
client *http.Client
18+
}
19+
20+
func (s Scanner) Version() int { return 1 }
21+
22+
var (
23+
// Ensure the Scanner satisfies the interface at compile time.
24+
_ detectors.Detector = (*Scanner)(nil)
25+
_ detectors.Versioner = (*Scanner)(nil)
26+
defaultClient = common.SaneHttpClient()
27+
28+
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
29+
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"brandfetch"}) + `\b([0-9A-Za-z]{40})\b`)
30+
)
31+
32+
// Keywords are used for efficiently pre-filtering chunks.
33+
// Use identifiers in the secret preferably, or the provider name.
34+
func (s Scanner) Keywords() []string {
35+
return []string{"brandfetch"}
36+
}
37+
38+
func (s Scanner) Type() detectorspb.DetectorType {
39+
return detectorspb.DetectorType_Brandfetch
40+
}
41+
42+
func (s Scanner) Description() string {
43+
return "Brandfetch is a service that provides brand data, including logos, colors, fonts, and more. Brandfetch API keys can be used to access this data."
44+
}
45+
46+
func (s Scanner) getClient() *http.Client {
47+
if s.client != nil {
48+
return s.client
49+
}
50+
51+
return defaultClient
52+
}
53+
54+
// FromData will find and optionally verify Brandfetch secrets in a given set of bytes.
55+
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
56+
dataStr := string(data)
57+
58+
uniqueTokenMatches := make(map[string]struct{})
59+
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
60+
uniqueTokenMatches[match[1]] = struct{}{}
61+
}
62+
63+
for match := range uniqueTokenMatches {
64+
s1 := detectors.Result{
65+
DetectorType: detectorspb.DetectorType_Brandfetch,
66+
Raw: []byte(match),
67+
ExtraData: map[string]string{"version": strconv.Itoa(s.Version())},
68+
}
69+
70+
if verify {
71+
isVerified, verificationErr := v2.VerifyMatch(ctx, s.getClient(), match)
72+
s1.Verified = isVerified
73+
s1.SetVerificationError(verificationErr, match)
74+
}
75+
76+
results = append(results, s1)
77+
}
78+
79+
return
80+
}

pkg/detectors/brandfetch/brandfetch_integration_test.go renamed to pkg/detectors/brandfetch/v1/brandfetch_integration_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ func TestBrandfetch_FromChunk(t *testing.T) {
9595
t.Fatalf("no raw secret present: \n %+v", got[i])
9696
}
9797
got[i].Raw = nil
98+
got[i].ExtraData = nil
9899
}
99100
if diff := pretty.Compare(got, tt.want); diff != "" {
100101
t.Errorf("Brandfetch.FromData() %s diff: (-got +want)\n%s", tt.name, diff)

pkg/detectors/brandfetch/brandfetch.go renamed to pkg/detectors/brandfetch/v2/brandfetch.go

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package brandfetch
33
import (
44
"context"
55
"fmt"
6-
"io"
76
"net/http"
7+
"strconv"
88
"strings"
99

1010
regexp "github.com/wasilibs/go-re2"
@@ -14,16 +14,20 @@ import (
1414
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
1515
)
1616

17-
type Scanner struct{}
17+
type Scanner struct {
18+
client *http.Client
19+
}
1820

19-
// Ensure the Scanner satisfies the interface at compile time.
20-
var _ detectors.Detector = (*Scanner)(nil)
21+
func (s Scanner) Version() int { return 2 }
2122

2223
var (
23-
client = common.SaneHttpClient()
24+
// Ensure the Scanner satisfies the interface at compile time.
25+
_ detectors.Detector = (*Scanner)(nil)
26+
_ detectors.Versioner = (*Scanner)(nil)
27+
defaultClient = common.SaneHttpClient()
2428

2529
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
26-
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"brandfetch"}) + `\b([0-9A-Za-z]{40})\b`)
30+
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"brandfetch"}) + `([a-zA-Z0-9=+/\-_!@#$%^&*()]{43}=)`)
2731
)
2832

2933
// Keywords are used for efficiently pre-filtering chunks.
@@ -32,63 +36,65 @@ func (s Scanner) Keywords() []string {
3236
return []string{"brandfetch"}
3337
}
3438

39+
func (s Scanner) Type() detectorspb.DetectorType {
40+
return detectorspb.DetectorType_Brandfetch
41+
}
42+
43+
func (s Scanner) Description() string {
44+
return "Brandfetch is a service that provides brand data, including logos, colors, fonts, and more. Brandfetch API keys can be used to access this data."
45+
}
46+
47+
func (s Scanner) getClient() *http.Client {
48+
if s.client != nil {
49+
return s.client
50+
}
51+
52+
return defaultClient
53+
}
54+
3555
// FromData will find and optionally verify Brandfetch secrets in a given set of bytes.
3656
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
3757
dataStr := string(data)
3858

39-
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
40-
41-
for _, match := range matches {
42-
resMatch := strings.TrimSpace(match[1])
59+
uniqueMatches := make(map[string]struct{})
60+
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
61+
uniqueMatches[strings.TrimSpace(match[1])] = struct{}{}
62+
}
4363

64+
for match := range uniqueMatches {
4465
s1 := detectors.Result{
4566
DetectorType: detectorspb.DetectorType_Brandfetch,
46-
Raw: []byte(resMatch),
67+
Raw: []byte(match),
68+
ExtraData: map[string]string{"version": strconv.Itoa(s.Version())},
4769
}
4870

4971
if verify {
50-
isVerified, verificationErr := verifyBrandFetch(ctx, client, resMatch)
72+
isVerified, verificationErr := VerifyMatch(ctx, s.getClient(), match)
5173
s1.Verified = isVerified
52-
s1.SetVerificationError(verificationErr)
74+
s1.SetVerificationError(verificationErr, match)
5375
}
5476

5577
results = append(results, s1)
5678
}
5779

58-
return results, nil
80+
return
5981
}
6082

61-
func (s Scanner) Type() detectorspb.DetectorType {
62-
return detectorspb.DetectorType_Brandfetch
63-
}
64-
65-
func (s Scanner) Description() string {
66-
return "Brandfetch is a service that provides brand data, including logos, colors, fonts, and more. Brandfetch API keys can be used to access this data."
67-
}
68-
69-
// docs: https://docs.brandfetch.com/docs/brand-api#overview
70-
func verifyBrandFetch(ctx context.Context, client *http.Client, key string) (bool, error) {
71-
payload := strings.NewReader(`{
72-
"domain": "www.example.com"
73-
}`)
74-
75-
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.brandfetch.io/v1/color", payload)
83+
// verifyMatch checks if the provided Brandfetch token is valid by making a request to the Brandfetch API.
84+
// https://docs.brandfetch.com/docs/getting-started
85+
func VerifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) {
86+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.brandfetch.io/v2/brands/google.com", http.NoBody)
7687
if err != nil {
7788
return false, err
7889
}
7990

8091
req.Header.Add("Content-Type", "application/json")
81-
req.Header.Add("x-api-key", key)
82-
92+
req.Header.Add("Authorization", "Bearer "+token)
8393
resp, err := client.Do(req)
8494
if err != nil {
8595
return false, err
8696
}
87-
88-
defer func() {
89-
_, _ = io.Copy(io.Discard, resp.Body)
90-
_ = resp.Body.Close()
91-
}()
97+
defer resp.Body.Close()
9298

9399
switch resp.StatusCode {
94100
case http.StatusOK:
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//go:build detectors
2+
// +build detectors
3+
4+
package brandfetch
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"testing"
10+
"time"
11+
12+
"github.com/kylelemons/godebug/pretty"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
14+
15+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
16+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
17+
)
18+
19+
func TestBrandfetch_FromChunk(t *testing.T) {
20+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
21+
defer cancel()
22+
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
23+
if err != nil {
24+
t.Fatalf("could not get test secrets from GCP: %s", err)
25+
}
26+
secret := testSecrets.MustGetField("BRANDFETCH_V2")
27+
inactiveSecret := testSecrets.MustGetField("BRANDFETCH_V2_INACTIVE")
28+
29+
type args struct {
30+
ctx context.Context
31+
data []byte
32+
verify bool
33+
}
34+
tests := []struct {
35+
name string
36+
s Scanner
37+
args args
38+
want []detectors.Result
39+
wantErr bool
40+
}{
41+
{
42+
name: "found, verified",
43+
s: Scanner{},
44+
args: args{
45+
ctx: context.Background(),
46+
data: []byte(fmt.Sprintf("You can find a brandfetch secret %s within", secret)),
47+
verify: true,
48+
},
49+
want: []detectors.Result{
50+
{
51+
DetectorType: detectorspb.DetectorType_Brandfetch,
52+
Verified: true,
53+
},
54+
},
55+
wantErr: false,
56+
},
57+
{
58+
name: "found, unverified",
59+
s: Scanner{},
60+
args: args{
61+
ctx: context.Background(),
62+
data: []byte(fmt.Sprintf("You can find a brandfetch secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
63+
verify: true,
64+
},
65+
want: []detectors.Result{
66+
{
67+
DetectorType: detectorspb.DetectorType_Brandfetch,
68+
Verified: false,
69+
},
70+
},
71+
wantErr: false,
72+
},
73+
{
74+
name: "not found",
75+
s: Scanner{},
76+
args: args{
77+
ctx: context.Background(),
78+
data: []byte("You cannot find the secret within"),
79+
verify: true,
80+
},
81+
want: nil,
82+
wantErr: false,
83+
},
84+
}
85+
for _, tt := range tests {
86+
t.Run(tt.name, func(t *testing.T) {
87+
s := Scanner{}
88+
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
89+
if (err != nil) != tt.wantErr {
90+
t.Errorf("Brandfetch.FromData() error = %v, wantErr %v", err, tt.wantErr)
91+
return
92+
}
93+
for i := range got {
94+
if len(got[i].Raw) == 0 {
95+
t.Fatalf("no raw secret present: \n %+v", got[i])
96+
}
97+
got[i].Raw = nil
98+
got[i].ExtraData = nil
99+
}
100+
if diff := pretty.Compare(got, tt.want); diff != "" {
101+
t.Errorf("Brandfetch.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
102+
}
103+
})
104+
}
105+
}
106+
107+
func BenchmarkFromData(benchmark *testing.B) {
108+
ctx := context.Background()
109+
s := Scanner{}
110+
for name, data := range detectors.MustGetBenchmarkData() {
111+
benchmark.Run(name, func(b *testing.B) {
112+
b.ResetTimer()
113+
for n := 0; n < b.N; n++ {
114+
_, err := s.FromData(ctx, false, data)
115+
if err != nil {
116+
b.Fatal(err)
117+
}
118+
}
119+
})
120+
}
121+
}

0 commit comments

Comments
 (0)