Skip to content

Commit c09d726

Browse files
[INS-497] Add Pganalyze Read Key Detector (#4993)
* Add pganalyze read key detector * regen protos and ran gofmt
1 parent c1a1d6a commit c09d726

9 files changed

Lines changed: 537 additions & 27 deletions

File tree

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ func run(state overseer.State, logSync func() error) {
543543
feature.RevDetectorEnabled.Store(true)
544544
feature.UserDetectorEnabled.Store(true)
545545
feature.BraintrustDetectorEnabled.Store(true)
546+
feature.PgAnalyzeReadKeyDetectorEnabled.Store(true)
546547

547548
conf := &config.Config{}
548549
if *configFilename != "" {
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package pganalyzereadkey
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
9+
regexp "github.com/wasilibs/go-re2"
10+
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
14+
)
15+
16+
type Scanner struct {
17+
client *http.Client
18+
}
19+
20+
// Compile-time interface check
21+
var _ detectors.Detector = (*Scanner)(nil)
22+
23+
var (
24+
defaultClient = common.SaneHttpClient()
25+
26+
// pganalyze Read API keys use the format:
27+
// pgar_<27 alphanumeric characters>
28+
//
29+
// Example:
30+
// pgar_abcdefghijklmnopqrstuvwxyz12
31+
pganalyzeTokenPat = regexp.MustCompile(
32+
`\b(pgar_[A-Za-z0-9]{27})\b`,
33+
)
34+
)
35+
36+
// Keywords used for fast pre-filtering
37+
func (s Scanner) Keywords() []string {
38+
return []string{
39+
"pgar_",
40+
}
41+
}
42+
43+
func (s Scanner) getClient() *http.Client {
44+
if s.client != nil {
45+
return s.client
46+
}
47+
return defaultClient
48+
}
49+
50+
// FromData scans for pganalyze API tokens and optionally verifies them.
51+
func (s Scanner) FromData(
52+
ctx context.Context,
53+
verify bool,
54+
data []byte,
55+
) (results []detectors.Result, err error) {
56+
57+
dataStr := string(data)
58+
59+
uniqueTokens := make(map[string]struct{})
60+
61+
matches := pganalyzeTokenPat.FindAllStringSubmatch(dataStr, -1)
62+
for _, match := range matches {
63+
uniqueTokens[match[1]] = struct{}{}
64+
}
65+
66+
for token := range uniqueTokens {
67+
result := detectors.Result{
68+
DetectorType: detector_typepb.DetectorType_PgAnalyzeReadKey,
69+
Raw: []byte(token),
70+
SecretParts: map[string]string{
71+
"key": token,
72+
"access_type": "read",
73+
},
74+
}
75+
76+
if verify {
77+
verified, verificationErr := verifyPganalyzeToken(
78+
ctx,
79+
s.getClient(),
80+
token,
81+
)
82+
83+
result.SetVerificationError(verificationErr, token)
84+
result.Verified = verified
85+
}
86+
87+
results = append(results, result)
88+
}
89+
90+
return
91+
}
92+
93+
func verifyPganalyzeToken(
94+
ctx context.Context,
95+
client *http.Client,
96+
token string,
97+
) (bool, error) {
98+
99+
req, err := http.NewRequestWithContext(
100+
ctx,
101+
http.MethodPost,
102+
"https://app.pganalyze.com/graphql",
103+
http.NoBody,
104+
)
105+
if err != nil {
106+
return false, err
107+
}
108+
109+
req.Header.Set("Authorization", "Token "+token)
110+
111+
res, err := client.Do(req)
112+
if err != nil {
113+
return false, err
114+
}
115+
116+
defer func() {
117+
_, _ = io.Copy(io.Discard, res.Body)
118+
_ = res.Body.Close()
119+
}()
120+
121+
switch res.StatusCode {
122+
123+
case http.StatusOK:
124+
return true, nil
125+
126+
// Explicit invalid auth
127+
case http.StatusUnauthorized:
128+
return false, nil
129+
130+
default:
131+
return false, fmt.Errorf(
132+
"unexpected HTTP response status %d",
133+
res.StatusCode,
134+
)
135+
}
136+
}
137+
138+
func (s Scanner) Type() detector_typepb.DetectorType {
139+
return detector_typepb.DetectorType_PgAnalyzeReadKey
140+
}
141+
142+
func (s Scanner) Description() string {
143+
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."
144+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
//go:build detectors
2+
// +build detectors
3+
4+
package pganalyzereadkey
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"testing"
10+
"time"
11+
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/google/go-cmp/cmp/cmpopts"
14+
15+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
16+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
17+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
18+
)
19+
20+
func TestPgAnalyzeReadKey_FromData(t *testing.T) {
21+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
22+
defer cancel()
23+
24+
testSecrets, err := common.GetSecret(
25+
ctx,
26+
"trufflehog-testing",
27+
"detectors6",
28+
)
29+
if err != nil {
30+
t.Fatalf("could not get test secrets from GCP: %s", err)
31+
}
32+
33+
activeToken := testSecrets.MustGetField("PGANALYZE_READ_KEY")
34+
inactiveToken := "pgar_abcdefghijklmnopqrstuvwxyz1"
35+
36+
type args struct {
37+
ctx context.Context
38+
data []byte
39+
verify bool
40+
}
41+
42+
tests := []struct {
43+
name string
44+
s Scanner
45+
args args
46+
want []detectors.Result
47+
wantErr bool
48+
wantVerificationErr bool
49+
}{
50+
{
51+
name: "found, verified",
52+
s: Scanner{},
53+
args: args{
54+
ctx: context.Background(),
55+
data: fmt.Appendf(
56+
[]byte{},
57+
"Using pganalyze read key %s for dashboard access",
58+
activeToken,
59+
),
60+
verify: true,
61+
},
62+
want: []detectors.Result{
63+
{
64+
DetectorType: detector_typepb.DetectorType_PgAnalyzeReadKey,
65+
Verified: true,
66+
Raw: []byte(activeToken),
67+
},
68+
},
69+
},
70+
{
71+
name: "found, real token, verification error due to timeout",
72+
s: Scanner{
73+
client: common.SaneHttpClientTimeOut(1 * time.Microsecond),
74+
},
75+
args: args{
76+
ctx: context.Background(),
77+
data: fmt.Appendf(
78+
[]byte{},
79+
"Using pganalyze read key %s for dashboard access",
80+
activeToken,
81+
),
82+
verify: true,
83+
},
84+
want: []detectors.Result{
85+
{
86+
DetectorType: detector_typepb.DetectorType_PgAnalyzeReadKey,
87+
Verified: false,
88+
Raw: []byte(activeToken),
89+
},
90+
},
91+
wantVerificationErr: true,
92+
},
93+
{
94+
name: "found, real token, verification error due to unexpected api surface",
95+
s: Scanner{
96+
client: common.ConstantResponseHttpClient(500, "{}"),
97+
},
98+
args: args{
99+
ctx: context.Background(),
100+
data: fmt.Appendf(
101+
[]byte{},
102+
"Using pganalyze read key %s for dashboard access",
103+
activeToken,
104+
),
105+
verify: true,
106+
},
107+
want: []detectors.Result{
108+
{
109+
DetectorType: detector_typepb.DetectorType_PgAnalyzeReadKey,
110+
Verified: false,
111+
Raw: []byte(activeToken),
112+
},
113+
},
114+
wantVerificationErr: true,
115+
},
116+
{
117+
name: "found, unverified (inactive token)",
118+
s: Scanner{},
119+
args: args{
120+
ctx: context.Background(),
121+
data: fmt.Appendf(
122+
[]byte{},
123+
"Using pganalyze read key %s for dashboard access",
124+
inactiveToken,
125+
),
126+
verify: true,
127+
},
128+
want: []detectors.Result{
129+
{
130+
DetectorType: detector_typepb.DetectorType_PgAnalyzeReadKey,
131+
Verified: false,
132+
Raw: []byte(inactiveToken),
133+
},
134+
},
135+
},
136+
{
137+
name: "not found",
138+
s: Scanner{},
139+
args: args{
140+
ctx: context.Background(),
141+
data: []byte("no secrets here"),
142+
verify: true,
143+
},
144+
want: nil,
145+
},
146+
}
147+
148+
for _, tt := range tests {
149+
t.Run(tt.name, func(t *testing.T) {
150+
got, err := tt.s.FromData(
151+
tt.args.ctx,
152+
tt.args.verify,
153+
tt.args.data,
154+
)
155+
156+
if (err != nil) != tt.wantErr {
157+
t.Fatalf(
158+
"PgAnalyzeReadKey.FromData() error = %v, wantErr %v",
159+
err,
160+
tt.wantErr,
161+
)
162+
}
163+
164+
for i := range got {
165+
if len(got[i].Raw) == 0 {
166+
t.Fatal("no raw secret present")
167+
}
168+
169+
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
170+
t.Fatalf(
171+
"wantVerificationError = %v, verification error = %v",
172+
tt.wantVerificationErr,
173+
got[i].VerificationError(),
174+
)
175+
}
176+
}
177+
178+
ignoreOpts := cmpopts.IgnoreFields(
179+
detectors.Result{},
180+
"ExtraData",
181+
"verificationError",
182+
"primarySecret",
183+
"SecretParts",
184+
"chunkOffset",
185+
"chunkOffsetSet",
186+
)
187+
188+
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
189+
t.Errorf(
190+
"PgAnalyzeReadKey.FromData() %s diff: (-got +want)\n%s",
191+
tt.name,
192+
diff,
193+
)
194+
}
195+
})
196+
}
197+
}
198+
199+
func BenchmarkPgAnalyzeReadKey_FromData(b *testing.B) {
200+
ctx := context.Background()
201+
s := Scanner{}
202+
203+
for name, data := range detectors.MustGetBenchmarkData() {
204+
b.Run(name, func(b *testing.B) {
205+
b.ResetTimer()
206+
207+
for n := 0; n < b.N; n++ {
208+
_, err := s.FromData(ctx, false, data)
209+
if err != nil {
210+
b.Fatal(err)
211+
}
212+
}
213+
})
214+
}
215+
}

0 commit comments

Comments
 (0)