Files
trufflehog/pkg/detectors/lob/lob_test.go
43b8e371aa Update regex pattern and 403 handling in the Lob detector to fix false positives (#5188)
* lob: exclude underscores from keyPat to fix false positives

* lob: restrict keyPat to lowercase hex

* distinguish forbidden responses by their API error code

* remove payment_method_unverified from 403 case

---------

Co-authored-by: Shahzad Haider <[email protected]>
Co-authored-by: Shahzad Haider <[email protected]>
2026-08-11 22:31:07 +05:00

208 lines
5.5 KiB
Go

package lob
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = "live_0979969b3f6cc23ed67e9b650bfaf64f710"
validPatternTest = "test_0979969b3f6cc23ed67e9b650bfaf64f710"
invalidPattern = "live_0979969b3f6cc23ed67e9b650bfaf64f71"
publishablePattern = "live_pub_0979969b3f6cc23ed67e9b650bfaf64"
)
func TestLob_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid live pattern",
input: fmt.Sprintf("token = '%s'", validPattern),
want: []string{validPattern},
},
{
name: "valid test pattern",
input: fmt.Sprintf("token = '%s'", validPatternTest),
want: []string{validPatternTest},
},
{
name: "valid pattern - ignore duplicate",
input: fmt.Sprintf("token = '%s' | '%s'", validPattern, validPattern),
want: []string{validPattern},
},
{
name: "invalid pattern",
input: fmt.Sprintf("'%s'", invalidPattern),
want: []string{},
},
{
name: "snake_case identifier of key length",
input: "def test_calculate_total_price_with_tax_rate():",
want: []string{},
},
{
name: "camelCase identifier of key length",
input: "func test_someVeryLongCamelCaseFunctionNameXy() {}",
want: []string{},
},
{
name: "publishable key",
input: fmt.Sprintf("token = '%s'", publishablePattern),
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("keywords '%v' not matched by: %s", d.Keywords(), test.input)
return
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
if err != nil {
t.Errorf("error = %v", err)
return
}
if len(results) != len(test.want) {
if len(results) == 0 {
t.Errorf("did not receive result")
} else {
t.Errorf("expected %d results, only received %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)
}
})
}
}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) }
func TestLob_Verify(t *testing.T) {
lobError := func(code string) string {
return fmt.Sprintf(`{"error":{"message":"...","status_code":403,"code":%q}}`, code)
}
tests := []struct {
name string
status int
contentType string
body string
want bool
wantErr bool
}{
{
name: "active key",
status: http.StatusUnprocessableEntity,
body: `{"error":{"message":"...","status_code":422,"code":"invalid"}}`,
want: true,
},
{
name: "invalid key",
status: http.StatusUnauthorized,
body: `{"error":{"message":"...","status_code":401,"code":"invalid_api_key"}}`,
want: false,
},
{
name: "active key without a billing address",
status: http.StatusForbidden,
body: lobError("billing_address_required"),
want: true,
},
{
name: "active key past its free request quota",
status: http.StatusForbidden,
body: lobError("feature_limit_reached"),
want: true,
},
{
name: "invalid key reported as forbidden",
status: http.StatusForbidden,
body: lobError("invalid_api_key"),
want: false,
},
{
name: "unrecognized error code",
status: http.StatusForbidden,
body: lobError("some_future_code"),
wantErr: true,
},
{
name: "forbidden without an API response",
status: http.StatusForbidden,
contentType: "text/html; charset=UTF-8",
body: "<!DOCTYPE html><html><head><title>Attention Required!</title></head></html>",
wantErr: true,
},
{
name: "unexpected status",
status: http.StatusInternalServerError,
body: `{"error":{"message":"...","status_code":500,"code":"internal_server_error"}}`,
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := Scanner{client: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
username, _, ok := req.BasicAuth()
if !ok || username != validPattern {
t.Errorf("basic auth username = %q (ok=%t), want %q", username, ok, validPattern)
}
contentType := test.contentType
if contentType == "" {
contentType = "application/json; charset=utf-8"
}
return &http.Response{
StatusCode: test.status,
Header: http.Header{"Content-Type": []string{contentType}},
Body: io.NopCloser(strings.NewReader(test.body)),
}, nil
})}}
got, err := s.verify(context.Background(), validPattern)
if (err != nil) != test.wantErr {
t.Fatalf("error = %v, wantErr %t", err, test.wantErr)
}
if got != test.want {
t.Errorf("verified = %t, want %t", got, test.want)
}
})
}
}