Skip to content

Commit 8b6aac3

Browse files
Added new version for heroku detector (#4201)
* Added new version for heroku detector * updated integration tests
1 parent f3b7c13 commit 8b6aac3

File tree

7 files changed

+355
-40
lines changed

7 files changed

+355
-40
lines changed

pkg/detectors/heroku/v1/heroku.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package heroku
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"strings"
9+
10+
regexp "github.com/wasilibs/go-re2"
11+
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
14+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
15+
)
16+
17+
type Scanner struct{}
18+
19+
// Ensure the Scanner satisfies the interface at compile time.
20+
var _ detectors.Detector = (*Scanner)(nil)
21+
var _ detectors.Versioner = (*Scanner)(nil)
22+
23+
var (
24+
client = common.SaneHttpClient()
25+
26+
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
27+
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"heroku"}) + `\b([0-9Aa-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b`)
28+
)
29+
30+
func (s Scanner) Version() int {
31+
return 1
32+
}
33+
34+
// Keywords are used for efficiently pre-filtering chunks.
35+
// Use identifiers in the secret preferably, or the provider name.
36+
func (s Scanner) Keywords() []string {
37+
return []string{"heroku"}
38+
}
39+
40+
// FromData will find and optionally verify Heroku secrets in a given set of bytes.
41+
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
42+
dataStr := string(data)
43+
44+
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
45+
46+
for _, match := range matches {
47+
resMatch := strings.TrimSpace(match[1])
48+
49+
s1 := detectors.Result{
50+
DetectorType: detectorspb.DetectorType_Heroku,
51+
Raw: []byte(resMatch),
52+
}
53+
54+
if verify {
55+
isVerified, verificationErr := VerifyHerokuAPIKey(ctx, client, resMatch)
56+
s1.Verified = isVerified
57+
s1.SetVerificationError(verificationErr)
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_Heroku
68+
}
69+
70+
func (s Scanner) Description() string {
71+
return "Heroku is a cloud platform that lets companies build, deliver, monitor and scale apps. Heroku API keys can be used to access and manage Heroku applications and services."
72+
}
73+
74+
func VerifyHerokuAPIKey(ctx context.Context, client *http.Client, key string) (bool, error) {
75+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.heroku.com/account", http.NoBody)
76+
if err != nil {
77+
return false, err
78+
}
79+
80+
req.Header.Add("Accept", "application/vnd.heroku+json; version=3")
81+
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key))
82+
83+
resp, err := client.Do(req)
84+
if err != nil {
85+
return false, err
86+
}
87+
88+
defer func() {
89+
_, _ = io.Copy(io.Discard, resp.Body)
90+
_ = resp.Body.Close()
91+
}()
92+
93+
switch resp.StatusCode {
94+
case http.StatusOK:
95+
return true, nil
96+
case http.StatusUnauthorized:
97+
return false, nil
98+
default:
99+
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
100+
}
101+
}

pkg/detectors/heroku/heroku_test.go renamed to pkg/detectors/heroku/v1/heroku_test.go

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,6 @@ import (
1010
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
1111
)
1212

13-
var (
14-
validPattern = `[{
15-
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
16-
"name": "Heroku",
17-
"type": "Detector",
18-
"api": true,
19-
"authentication_type": "",
20-
"verification_url": "https://api.example.com/example",
21-
"test_secrets": {
22-
"heroku_secret": "bAf8bA7d-7088-07ce-3f87-7ec21653297d"
23-
},
24-
"expected_response": "200",
25-
"method": "GET",
26-
"deprecated": false
27-
}]`
28-
secret = "bAf8bA7d-7088-07ce-3f87-7ec21653297d"
29-
)
30-
3113
func TestHeroku_Pattern(t *testing.T) {
3214
d := Scanner{}
3315
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
@@ -38,9 +20,22 @@ func TestHeroku_Pattern(t *testing.T) {
3820
want []string
3921
}{
4022
{
41-
name: "valid pattern",
42-
input: validPattern,
43-
want: []string{secret},
23+
name: "valid pattern",
24+
input: `[{
25+
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
26+
"name": "Heroku",
27+
"type": "Detector",
28+
"api": true,
29+
"authentication_type": "",
30+
"verification_url": "https://api.example.com/example",
31+
"test_secrets": {
32+
"heroku_secret": "bAf8bA7d-7088-07ce-3f87-7ec21653297d"
33+
},
34+
"expected_response": "200",
35+
"method": "GET",
36+
"deprecated": false
37+
}]`,
38+
want: []string{"bAf8bA7d-7088-07ce-3f87-7ec21653297d"},
4439
},
4540
}
4641

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

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,37 @@ package heroku
22

33
import (
44
"context"
5-
"fmt"
6-
"net/http"
75
"strings"
86

97
regexp "github.com/wasilibs/go-re2"
108

119
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
1210
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
11+
v1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/heroku/v1"
1312
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
1413
)
1514

1615
type Scanner struct{}
1716

1817
// Ensure the Scanner satisfies the interface at compile time.
1918
var _ detectors.Detector = (*Scanner)(nil)
19+
var _ detectors.Versioner = (*Scanner)(nil)
2020

2121
var (
2222
client = common.SaneHttpClient()
2323

2424
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
25-
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"heroku"}) + `\b([0-9Aa-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b`)
25+
keyPat = regexp.MustCompile(`\b(HRKU-AA[0-9a-zA-Z_-]{58})\b`)
2626
)
2727

28+
func (s Scanner) Version() int {
29+
return 2
30+
}
31+
2832
// Keywords are used for efficiently pre-filtering chunks.
2933
// Use identifiers in the secret preferably, or the provider name.
3034
func (s Scanner) Keywords() []string {
31-
return []string{"heroku"}
35+
return []string{"HRKU-AA"}
3236
}
3337

3438
// FromData will find and optionally verify Heroku secrets in a given set of bytes.
@@ -46,19 +50,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
4650
}
4751

4852
if verify {
49-
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.heroku.com/apps", nil)
50-
if err != nil {
51-
continue
52-
}
53-
req.Header.Add("Accept", "application/vnd.heroku+json; version=3")
54-
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
55-
res, err := client.Do(req)
56-
if err == nil {
57-
defer res.Body.Close()
58-
if res.StatusCode >= 200 && res.StatusCode < 300 {
59-
s1.Verified = true
60-
}
61-
}
53+
isVerified, verificationErr := v1.VerifyHerokuAPIKey(ctx, client, resMatch)
54+
s1.Verified = isVerified
55+
s1.SetVerificationError(verificationErr)
6256
}
6357

6458
results = append(results, s1)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
//go:build detectors
2+
// +build detectors
3+
4+
package heroku
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"testing"
10+
"time"
11+
12+
"github.com/kylelemons/godebug/pretty"
13+
14+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
15+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
16+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
17+
)
18+
19+
func TestHeroku_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("HEROKU_V2_TOKEN")
27+
inactiveSecret := testSecrets.MustGetField("HEROKU_V2_TOKEN_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 heroku secret %s within", secret)),
47+
verify: true,
48+
},
49+
want: []detectors.Result{
50+
{
51+
DetectorType: detectorspb.DetectorType_Heroku,
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 heroku 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_Heroku,
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("Heroku.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+
}
99+
if diff := pretty.Compare(got, tt.want); diff != "" {
100+
t.Errorf("Heroku.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
101+
}
102+
})
103+
}
104+
}
105+
106+
func BenchmarkFromData(benchmark *testing.B) {
107+
ctx := context.Background()
108+
s := Scanner{}
109+
for name, data := range detectors.MustGetBenchmarkData() {
110+
benchmark.Run(name, func(b *testing.B) {
111+
b.ResetTimer()
112+
for n := 0; n < b.N; n++ {
113+
_, err := s.FromData(ctx, false, data)
114+
if err != nil {
115+
b.Fatal(err)
116+
}
117+
}
118+
})
119+
}
120+
}

0 commit comments

Comments
 (0)