Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions pkg/detectors/grafana/grafana.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@
var (
defaultClient = common.SaneHttpClient()
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(glc_eyJ[A-Za-z0-9+\/=]{60,160})`)
)
keyPat = regexp.MustCompile(\b(glc_eyJ[A-Za-z0-9+/]{29,400}={0,2})\b))

Check failure on line 27 in pkg/detectors/grafana/grafana.go

View workflow job for this annotation

GitHub Actions / zombies

syntax error: unexpected /, expected expression

Check failure on line 27 in pkg/detectors/grafana/grafana.go

View workflow job for this annotation

GitHub Actions / zombies

invalid character U+005C '\'

Check failure on line 27 in pkg/detectors/grafana/grafana.go

View workflow job for this annotation

GitHub Actions / smoke

syntax error: unexpected /, expected expression

Check failure on line 27 in pkg/detectors/grafana/grafana.go

View workflow job for this annotation

GitHub Actions / smoke

invalid character U+005C '\'

Check failure on line 27 in pkg/detectors/grafana/grafana.go

View workflow job for this annotation

GitHub Actions / Analyze (go)

syntax error: unexpected /, expected expression

Check failure on line 27 in pkg/detectors/grafana/grafana.go

View workflow job for this annotation

GitHub Actions / Analyze (go)

invalid character U+005C '\'

Check failure on line 27 in pkg/detectors/grafana/grafana.go

View workflow job for this annotation

GitHub Actions / golangci-lint

illegal character U+005C '\' (typecheck)

Check failure on line 27 in pkg/detectors/grafana/grafana.go

View workflow job for this annotation

GitHub Actions / golangci-lint

syntax error: unexpected /, expected expression (typecheck)

Check failure on line 27 in pkg/detectors/grafana/grafana.go

View workflow job for this annotation

GitHub Actions / golangci-lint

invalid character U+005C '\'

Check failure on line 27 in pkg/detectors/grafana/grafana.go

View workflow job for this annotation

GitHub Actions / test-community

syntax error: unexpected /, expected expression

Check failure on line 27 in pkg/detectors/grafana/grafana.go

View workflow job for this annotation

GitHub Actions / test-community

invalid character U+005C '\'

func (s Scanner) getClient() *http.Client {
client := s.client
if client == nil {

Check failure on line 31 in pkg/detectors/grafana/grafana.go

View workflow job for this annotation

GitHub Actions / golangci-lint

missing ',' in argument list (typecheck)
client = defaultClient

Check failure on line 32 in pkg/detectors/grafana/grafana.go

View workflow job for this annotation

GitHub Actions / golangci-lint

expected '==', found '=' (typecheck)
}

return client
Expand All @@ -38,6 +37,8 @@

// Keywords are used for efficiently pre-filtering chunks.
// Use identifiers in the secret preferably, or the provider name.
// Grafana uses "glc_" as a prefix for cloud API tokens, see for example.
// https://github.com/grafana/pyroscope-dotnet/blob/0c17634653af09befa7bc07b2e1c420b5dc8578c/tracer/src/Datadog.Trace/Iast/Analyzers/HardcodedSecretsAnalyzer.cs#L174
func (s Scanner) Keywords() []string {
return []string{"glc_eyJ"}
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/detectors/grafana/grafana_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ var (
"authentication_type": "",
"verification_url": "https://api.example.com/example",
"test_secrets": {
"grafana_secret": "glc_eyJF057+C0x9J+QwzC5JXb5uQ/WSzn98X/iIrZXKaA3Hh+lum0XBRcu56qMlW7ZaxXrNt33XoI3CXz7IRPci="
"grafana_secret": "glc_eyJF057+C0x9J+QwzC5JXb5uQ/WSzn98X/iIrZXKaA3Hh+lum0XBRcu56qMlW7ZaxXrNt33XoI3CXz7IRPci"
},
"expected_response": "200",
"method": "GET",
"deprecated": false
}]`
secret = "glc_eyJF057+C0x9J+QwzC5JXb5uQ/WSzn98X/iIrZXKaA3Hh+lum0XBRcu56qMlW7ZaxXrNt33XoI3CXz7IRPci="
secret = "glc_eyJF057+C0x9J+QwzC5JXb5uQ/WSzn98X/iIrZXKaA3Hh+lum0XBRcu56qMlW7ZaxXrNt33XoI3CXz7IRPci"
)

func TestGrafana_Pattern(t *testing.T) {
Expand Down
63 changes: 63 additions & 0 deletions pkg/detectors/grafanaapikey/grafanaapikey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package grafanaapikey

import (
"context"
"fmt"

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

"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)

type Scanner struct {
}

// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)

var (
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(`\b(eyJrIjoi[A-Za-z0-9]{70,400}={0,2})\b`)
)

// Keywords are used for efficiently pre-filtering chunks.
// Use identifiers in the secret preferably, or the provider name.
// Grafana uses "eyJrIjoi" as a prefix for api keys, see for example.
// https://github.com/grafana/pyroscope-dotnet/blob/0c17634653af09befa7bc07b2e1c420b5dc8578c/tracer/src/Datadog.Trace/Iast/Analyzers/HardcodedSecretsAnalyzer.cs#L173
func (s Scanner) Keywords() []string {
return []string{"grafanaapikey", "eyJrIjoi"}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use "eyJrIjoi" exclusively since it's always part of the key. A keyword match for "grafanaapikey" won't necessarily contain eyJrIjoi in its input.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh. PEBKAC.... I'll update that :P

}

// FromData will find and optionally verify Grafanaapikey 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)

uniqueMatches := make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueMatches[match[1]] = struct{}{}
}

for match := range uniqueMatches {
res := detectors.Result{
DetectorType: detectorspb.DetectorType_GrafanaAPIKey,
Raw: []byte(match),
}

if verify {
res.SetVerificationError(fmt.Errorf("no grafana instance detected to verify against"), match)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't seem to be verifying the matched credentials

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These kind of api keys are used for on-premise installations, so together with a custom endpoint that could be an IP address or a FQDN. Since it's hard to reliably identify it - there is no good way of verifying the matched credentials.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verification logic has to exist in a detector. In docs, it is mentioned that Grafana can be hosted on a VPS or as a subdomain. Gitlab detector is the best example for this

}

results = append(results, res)
}

return
}

func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_GrafanaAPIKey
}

func (s Scanner) Description() string {
return "Grafana API keys are used to authenticate and interact with Grafana's API. These credentials can be used to access and modify Grafana resources and settings."
}
161 changes: 161 additions & 0 deletions pkg/detectors/grafanaapikey/grafanaapikey_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//go:build detectors
// +build detectors

package grafanaapikey

import (
"context"
"fmt"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"

"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)

func TestGrafanaapikey_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("GRAFANAAPIKEY")
inactiveSecret := testSecrets.MustGetField("GRAFANAAPIKEY_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 grafanaapikey secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_GrafanaAPIKey,
Verified: true,
},
},
wantErr: false,
wantVerificationErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a grafanaapikey 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_GrafanaAPIKey,
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 grafanaapikey secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_GrafanaAPIKey,
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 grafanaapikey secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_GrafanaAPIKey,
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("Grafanaapikey.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("Grafanaapikey.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)
}
}
})
}
}
102 changes: 102 additions & 0 deletions pkg/detectors/grafanaapikey/grafanaapikey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package grafanaapikey

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"

"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)

func TestGrafanaapikey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: `
[INFO] Sending request to the grafanaapikey API
[DEBUG] Using Key=eyJrIjoideadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
[INFO] Response received: 200 OK
`,
want: []string{"eyJrIjoideadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"},
},
{
name: "valid pattern - xml",
input: `
<com.cloudbees.plugins.credentials.impl.StringCredentialsImpl>
<scope>GLOBAL</scope>
<id>{grafanaapikey}</id>
<secret>{grafanaapikey AQAAABAAA eyJrIjoideadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef}</secret>
<description>configuration for production</description>
<creationDate>2023-05-18T14:32:10Z</creationDate>
<owner>jenkins-admin</owner>
</com.cloudbees.plugins.credentials.impl.StringCredentialsImpl>
`,
want: []string{"eyJrIjoideadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"},
},
{
name: "finds all matches",
input: `
[INFO] Sending request to the grafanaapikey API
[DEBUG] Using Key=eyJrIjoideadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
[ERROR] Response received 401 UnAuthorized
[DEBUG] Using grafanaapikey Key=eyJrIjoiBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBAD
[INFO] Response received: 200 OK
`,
want: []string{"eyJrIjoideadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "eyJrIjoiBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBADBAD"},
},
{
name: "invalid pattern",
input: `
[INFO] Sending request to the grafanaapikey API
[DEBUG] Using Key=jkekfjskekfjedieksmfjfieke
[ERROR] Response received: 401 UnAuthorized
`,
want: []string{},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("test %q failed: expected keywords %v to be found in the input", test.name, d.Keywords())
return
}

results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)

if len(results) != len(test.want) {
t.Errorf("mismatch in result count: expected %d, got %d", len(test.want), len(results))
return
}

actual := make(map[string]struct{}, len(results))
for _, r := range results {
if len(r.RawV2) > 0 {
actual[string(r.RawV2)] = struct{}{}
} else {
actual[string(r.Raw)] = struct{}{}
}
}

expected := make(map[string]struct{}, len(test.want))
for _, v := range test.want {
expected[v] = struct{}{}
}

if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
}
})
}
}
Loading
Loading