Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ func run(state overseer.State, logSync func() error) {
feature.PineconeDetectorEnabled.Store(true)
feature.CloudinaryDetectorEnabled.Store(true)
feature.GitLabOAuthDetectorEnabled.Store(true)
feature.PgAnalyzeReadKeyDetectorEnabled.Store(true)

conf := &config.Config{}
if *configFilename != "" {
Expand Down
144 changes: 144 additions & 0 deletions pkg/detectors/pganalyzereadkey/pganalyzereadkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package pganalyzereadkey

import (
"context"
"fmt"
"io"
"net/http"

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

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

type Scanner struct {
client *http.Client
}

// Compile-time interface check
var _ detectors.Detector = (*Scanner)(nil)

var (
defaultClient = common.SaneHttpClient()

// pganalyze Read API keys use the format:
// pgar_<27 alphanumeric characters>
//
// Example:
// pgar_abcdefghijklmnopqrstuvwxyz12
pganalyzeTokenPat = regexp.MustCompile(
`\b(pgar_[A-Za-z0-9]{27})\b`,
)
)

// Keywords used for fast pre-filtering
func (s Scanner) Keywords() []string {
return []string{
"pgar_",
}
}

func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}

// FromData scans for pganalyze API tokens and optionally verifies them.
func (s Scanner) FromData(
ctx context.Context,
verify bool,
data []byte,
) (results []detectors.Result, err error) {

dataStr := string(data)

uniqueTokens := make(map[string]struct{})

matches := pganalyzeTokenPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
uniqueTokens[match[1]] = struct{}{}
}

for token := range uniqueTokens {
result := detectors.Result{
DetectorType: detector_typepb.DetectorType_PgAnalyzeReadKey,
Raw: []byte(token),
SecretParts: map[string]string{
"key": token,
"access_type": "read",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it's worth adding this to the ExtraData as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since the detector name is PgAnalyzeReadKey, which already indicates that this is a read-only key, what additional value does access_type: read provide in the secret metadata?

Also, can this secret type have other permission levels (e.g. write or admin), or is it always read-only?

},
}

if verify {
verified, verificationErr := verifyPganalyzeToken(
ctx,
s.getClient(),
token,
)

result.SetVerificationError(verificationErr, token)
result.Verified = verified
}

results = append(results, result)
}

return
}

func verifyPganalyzeToken(
ctx context.Context,
client *http.Client,
token string,
) (bool, error) {

req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
"https://app.pganalyze.com/graphql",
http.NoBody,
)
if err != nil {
return false, err
}

req.Header.Set("Authorization", "Token "+token)

res, err := client.Do(req)
if err != nil {
return false, err
}

defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()

switch res.StatusCode {

case http.StatusOK:
return true, nil

// Explicit invalid auth
case http.StatusUnauthorized:
return false, nil

default:
return false, fmt.Errorf(
"unexpected HTTP response status %d",
res.StatusCode,
)
}
}

func (s Scanner) Type() detector_typepb.DetectorType {
return detector_typepb.DetectorType_PgAnalyzeReadKey
}

func (s Scanner) Description() string {
return "pganalyze is a PostgreSQL monitoring and performance analysis platform. pganalyze Read API keys can be used to access read-only monitoring and query performance data."
}
215 changes: 215 additions & 0 deletions pkg/detectors/pganalyzereadkey/pganalyzereadkey_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
//go:build detectors
// +build detectors

package pganalyzereadkey

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/detector_typepb"
)

func TestPgAnalyzeReadKey_FromData(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

testSecrets, err := common.GetSecret(
ctx,
"trufflehog-testing",
"detectors6",
)
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}

activeToken := testSecrets.MustGetField("PGANALYZE_READ_KEY")
inactiveToken := "pgar_abcdefghijklmnopqrstuvwxyz1"

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: fmt.Appendf(
[]byte{},
"Using pganalyze read key %s for dashboard access",
activeToken,
),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detector_typepb.DetectorType_PgAnalyzeReadKey,
Verified: true,
Raw: []byte(activeToken),
},
},
},
{
name: "found, real token, verification error due to timeout",
s: Scanner{
client: common.SaneHttpClientTimeOut(1 * time.Microsecond),
},
args: args{
ctx: context.Background(),
data: fmt.Appendf(
[]byte{},
"Using pganalyze read key %s for dashboard access",
activeToken,
),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detector_typepb.DetectorType_PgAnalyzeReadKey,
Verified: false,
Raw: []byte(activeToken),
},
},
wantVerificationErr: true,
},
{
name: "found, real token, verification error due to unexpected api surface",
s: Scanner{
client: common.ConstantResponseHttpClient(500, "{}"),
},
args: args{
ctx: context.Background(),
data: fmt.Appendf(
[]byte{},
"Using pganalyze read key %s for dashboard access",
activeToken,
),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detector_typepb.DetectorType_PgAnalyzeReadKey,
Verified: false,
Raw: []byte(activeToken),
},
},
wantVerificationErr: true,
},
{
name: "found, unverified (inactive token)",
s: Scanner{},
args: args{
ctx: context.Background(),
data: fmt.Appendf(
[]byte{},
"Using pganalyze read key %s for dashboard access",
inactiveToken,
),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detector_typepb.DetectorType_PgAnalyzeReadKey,
Verified: false,
Raw: []byte(inactiveToken),
},
},
},
{
name: "not found",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte("no secrets here"),
verify: true,
},
want: nil,
},
}

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.Fatalf(
"PgAnalyzeReadKey.FromData() error = %v, wantErr %v",
err,
tt.wantErr,
)
}

for i := range got {
if len(got[i].Raw) == 0 {
t.Fatal("no raw secret present")
}

if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
t.Fatalf(
"wantVerificationError = %v, verification error = %v",
tt.wantVerificationErr,
got[i].VerificationError(),
)
}
}

ignoreOpts := cmpopts.IgnoreFields(
detectors.Result{},
"ExtraData",
"verificationError",
"primarySecret",
"SecretParts",
"chunkOffset",
"chunkOffsetSet",
)

if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf(
"PgAnalyzeReadKey.FromData() %s diff: (-got +want)\n%s",
tt.name,
diff,
)
}
})
}
}

func BenchmarkPgAnalyzeReadKey_FromData(b *testing.B) {
ctx := context.Background()
s := Scanner{}

for name, data := range detectors.MustGetBenchmarkData() {
b.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)
}
}
})
}
}
Loading
Loading