Skip to content

Commit fdaaacc

Browse files
bannerbear detector v2 added
1 parent 190f454 commit fdaaacc

File tree

7 files changed

+346
-3
lines changed

7 files changed

+346
-3
lines changed

pkg/detectors/bannerbear/bannerbear.go renamed to pkg/detectors/bannerbear/v1/bannerbear.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ import (
1616

1717
type Scanner struct{}
1818

19+
func (s Scanner) Version() int { return 1 }
20+
1921
// Ensure the Scanner satisfies the interface at compile time.
2022
var _ detectors.Detector = (*Scanner)(nil)
23+
var _ detectors.Versioner = (*Scanner)(nil)
2124

2225
var (
2326
client = common.SaneHttpClient()
@@ -44,6 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
4447
s1 := detectors.Result{
4548
DetectorType: detectorspb.DetectorType_Bannerbear,
4649
Raw: []byte(resMatch),
50+
ExtraData: map[string]string{
51+
"version": fmt.Sprintf("%d", s.Version()),
52+
},
4753
}
4854

4955
if verify {
@@ -77,7 +83,7 @@ func verifyBannerBear(ctx context.Context, client *http.Client, key string) (boo
7783

7884
resp, err := client.Do(req)
7985
if err != nil {
80-
return false, nil
86+
return false, err
8187
}
8288

8389
defer func() {

pkg/detectors/bannerbear/bannerbear_integration_test.go renamed to pkg/detectors/bannerbear/v1/bannerbear_integration_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ func TestBannerbear_FromChunk(t *testing.T) {
5050
{
5151
DetectorType: detectorspb.DetectorType_Bannerbear,
5252
Verified: true,
53+
ExtraData: map[string]string{
54+
"version": fmt.Sprintf("%d", 1),
55+
},
5356
},
5457
},
5558
wantErr: false,
@@ -66,6 +69,9 @@ func TestBannerbear_FromChunk(t *testing.T) {
6669
{
6770
DetectorType: detectorspb.DetectorType_Bannerbear,
6871
Verified: false,
72+
ExtraData: map[string]string{
73+
"version": fmt.Sprintf("%d", 1),
74+
},
6975
},
7076
},
7177
wantErr: false,
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package bannerbear
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"strings"
10+
11+
regexp "github.com/wasilibs/go-re2"
12+
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
14+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
15+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
16+
)
17+
18+
type Scanner struct{}
19+
20+
func (s Scanner) Version() int { return 2 }
21+
22+
// Ensure the Scanner satisfies the interface at compile time.
23+
var _ detectors.Detector = (*Scanner)(nil)
24+
var _ detectors.Versioner = (*Scanner)(nil)
25+
26+
var (
27+
client = common.SaneHttpClient()
28+
29+
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
30+
keyPat = regexp.MustCompile(`\b(bb_(?:pr|ma)_[a-f0-9]{30})\b`)
31+
)
32+
33+
// Keywords are used for efficiently pre-filtering chunks.
34+
// Use identifiers in the secret preferably, or the provider name.
35+
func (s Scanner) Keywords() []string {
36+
return []string{"bannerbear", "bb_pr_", "bb_ma_"}
37+
}
38+
39+
// FromData will find and optionally verify Bannerbear secrets in a given set of bytes.
40+
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
41+
dataStr := string(data)
42+
43+
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
44+
45+
for _, match := range matches {
46+
resMatch := strings.TrimSpace(match[1])
47+
48+
s1 := detectors.Result{
49+
DetectorType: detectorspb.DetectorType_Bannerbear,
50+
Raw: []byte(resMatch),
51+
}
52+
53+
if verify {
54+
isVerified, extraData, verificationErr := s.verifyBannerBear(ctx, client, resMatch)
55+
s1.Verified = isVerified
56+
s1.ExtraData = extraData
57+
s1.SetVerificationError(verificationErr, resMatch)
58+
}
59+
60+
results = append(results, s1)
61+
}
62+
63+
return results, nil
64+
}
65+
66+
func (s Scanner) Type() detectorspb.DetectorType {
67+
return detectorspb.DetectorType_Bannerbear
68+
}
69+
70+
func (s Scanner) Description() string {
71+
return "Bannerbear is an API for generating dynamic images, videos, and GIFs. Bannerbear API keys can be used to access and manipulate these resources."
72+
}
73+
74+
// docs: https://developers.bannerbear.com/
75+
func (s Scanner) verifyBannerBear(ctx context.Context, client *http.Client, key string) (bool, map[string]string, error) {
76+
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bannerbear.com/v2/auth", http.NoBody)
77+
if err != nil {
78+
return false, nil, err
79+
}
80+
81+
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
82+
83+
resp, err := client.Do(req)
84+
if err != nil {
85+
return false, nil, err
86+
}
87+
88+
defer func() {
89+
_, _ = io.Copy(io.Discard, resp.Body)
90+
_ = resp.Body.Close()
91+
}()
92+
93+
extraData := map[string]string{"version": fmt.Sprintf("%d", s.Version())}
94+
95+
switch resp.StatusCode {
96+
case http.StatusOK:
97+
extraData["key_type"] = "Project API Key"
98+
return true, extraData, nil
99+
case http.StatusBadRequest:
100+
bodyBytes, err := io.ReadAll(resp.Body)
101+
if err != nil {
102+
return false, extraData, err
103+
}
104+
105+
// According to Bannerbear API docs (https://developers.bannerbear.com/#authentication), the /auth endpoint
106+
// expects us to add a project_id parameter to the payload, when using a Full Access Master API Key.
107+
// otherwise, it returns a 400 Bad Request with "Error: When using a Master API Key you must set a project_id parameter"
108+
// Also, when we use a Master API Key with limited access, it returns a 400 Bad Request with "Error: this Master Key is Limited Access only"
109+
validResponse := bytes.Contains(bodyBytes, []byte("When using a Master API Key")) || bytes.Contains(bodyBytes, []byte("Master Key is Limited Access"))
110+
if validResponse {
111+
extraData["key_type"] = "Master API Key"
112+
return true, extraData, nil
113+
} else {
114+
return false, extraData, fmt.Errorf("bad request: %s, body: %s", resp.Status, string(bodyBytes))
115+
}
116+
case http.StatusUnauthorized:
117+
return false, extraData, nil
118+
default:
119+
return false, extraData, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
120+
}
121+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//go:build detectors
2+
// +build detectors
3+
4+
package bannerbear
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 TestBannerbear_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", "detectors5")
23+
if err != nil {
24+
t.Fatalf("could not get test secrets from GCP: %s", err)
25+
}
26+
secret := testSecrets.MustGetField("BANNERBEARV2")
27+
inactiveSecret := testSecrets.MustGetField("BANNERBEARV2_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 bannerbear secret %s within", secret)),
47+
verify: true,
48+
},
49+
want: []detectors.Result{
50+
{
51+
DetectorType: detectorspb.DetectorType_Bannerbear,
52+
Verified: true,
53+
ExtraData: map[string]string{
54+
"version": fmt.Sprintf("%d", 2),
55+
"key_type": "Project API Key",
56+
},
57+
},
58+
},
59+
wantErr: false,
60+
},
61+
{
62+
name: "found, unverified",
63+
s: Scanner{},
64+
args: args{
65+
ctx: context.Background(),
66+
data: []byte(fmt.Sprintf("You can find a bannerbear secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
67+
verify: true,
68+
},
69+
want: []detectors.Result{
70+
{
71+
DetectorType: detectorspb.DetectorType_Bannerbear,
72+
Verified: false,
73+
ExtraData: map[string]string{
74+
"version": fmt.Sprintf("%d", 2),
75+
},
76+
},
77+
},
78+
wantErr: false,
79+
},
80+
{
81+
name: "not found",
82+
s: Scanner{},
83+
args: args{
84+
ctx: context.Background(),
85+
data: []byte("You cannot find the secret within"),
86+
verify: true,
87+
},
88+
want: nil,
89+
wantErr: false,
90+
},
91+
}
92+
for _, tt := range tests {
93+
t.Run(tt.name, func(t *testing.T) {
94+
s := Scanner{}
95+
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
96+
if (err != nil) != tt.wantErr {
97+
t.Errorf("Bannerbear.FromData() error = %v, wantErr %v", err, tt.wantErr)
98+
return
99+
}
100+
for i := range got {
101+
if len(got[i].Raw) == 0 {
102+
t.Fatalf("no raw secret present: \n %+v", got[i])
103+
}
104+
got[i].Raw = nil
105+
}
106+
if diff := pretty.Compare(got, tt.want); diff != "" {
107+
t.Errorf("Bannerbear.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
108+
}
109+
})
110+
}
111+
}
112+
113+
func BenchmarkFromData(benchmark *testing.B) {
114+
ctx := context.Background()
115+
s := Scanner{}
116+
for name, data := range detectors.MustGetBenchmarkData() {
117+
benchmark.Run(name, func(b *testing.B) {
118+
b.ResetTimer()
119+
for n := 0; n < b.N; n++ {
120+
_, err := s.FromData(ctx, false, data)
121+
if err != nil {
122+
b.Fatal(err)
123+
}
124+
}
125+
})
126+
}
127+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package bannerbear
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/google/go-cmp/cmp"
9+
10+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
12+
)
13+
14+
func TestBannerBear_Pattern(t *testing.T) {
15+
d := Scanner{}
16+
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
17+
18+
tests := []struct {
19+
name string
20+
input string
21+
want []string
22+
}{
23+
{
24+
name: "valid pattern",
25+
input: fmt.Sprintf("bannerbear credentials: bb_pr_abcdc2b40ef44abcd8cbf3739aabcd"),
26+
want: []string{"bb_pr_abcdc2b40ef44abcd8cbf3739aabcd"},
27+
},
28+
{
29+
name: "valid pattern - complex",
30+
input: fmt.Sprintf("bannerbear credentials: ajahf ajkahfkjah fka bb_pr_abcdc2b40ef44abcd8cbf3739aacba adlkajflaihflahdljajfla"),
31+
want: []string{"bb_pr_abcdc2b40ef44abcd8cbf3739aacba"},
32+
},
33+
{
34+
name: "invalid pattern",
35+
input: fmt.Sprintf("bannerbear credentials: bb_abcdc2b40ef44abcd8cbf3739aabcdef1"),
36+
want: nil,
37+
},
38+
}
39+
40+
for _, test := range tests {
41+
t.Run(test.name, func(t *testing.T) {
42+
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
43+
if len(matchedDetectors) == 0 {
44+
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
45+
return
46+
}
47+
48+
results, err := d.FromData(context.Background(), false, []byte(test.input))
49+
if err != nil {
50+
t.Errorf("error = %v", err)
51+
return
52+
}
53+
54+
if len(results) != len(test.want) {
55+
if len(results) == 0 {
56+
t.Errorf("did not receive result")
57+
} else {
58+
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
59+
}
60+
return
61+
}
62+
63+
actual := make(map[string]struct{}, len(results))
64+
for _, r := range results {
65+
if len(r.RawV2) > 0 {
66+
actual[string(r.RawV2)] = struct{}{}
67+
} else {
68+
actual[string(r.Raw)] = struct{}{}
69+
}
70+
}
71+
expected := make(map[string]struct{}, len(test.want))
72+
for _, v := range test.want {
73+
expected[v] = struct{}{}
74+
}
75+
76+
if diff := cmp.Diff(expected, actual); diff != "" {
77+
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
78+
}
79+
})
80+
}
81+
}

0 commit comments

Comments
 (0)