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
65 changes: 48 additions & 17 deletions pkg/detectors/lob/lob.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package lob

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

Expand All @@ -12,22 +13,31 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
)

type Scanner struct{}
type Scanner struct {
client *http.Client
}

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

var (
client = common.SaneHttpClient()
defaultClient = common.SaneHttpClient()

// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"lob"}) + `\b([a-zA-Z0-9_]{40})\b`)
keyPat = regexp.MustCompile(`\b((live|test)_[a-zA-Z0-9_]{35})\b`)
)

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

// Keywords are used for efficiently pre-filtering chunks.
// Use identifiers in the secret preferably, or the provider name.
func (s Scanner) Keywords() []string {
return []string{"lob"}
return []string{"live_", "test_"}
Comment thread
shahzadhaider1 marked this conversation as resolved.
}

// FromData will find and optionally verify Lob secrets in a given set of bytes.
Expand All @@ -36,28 +46,25 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result

matches := keyPat.FindAllStringSubmatch(dataStr, -1)

uniqueMatches := make(map[string]struct{})
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])
uniqueMatches[strings.TrimSpace(match[1])] = struct{}{}
}

for resMatch := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detector_typepb.DetectorType_Lob,
Raw: []byte(resMatch),
SecretParts: map[string]string{"key": resMatch},
ExtraData: map[string]string{
"environment": resMatch[:4], // live or test
},
}

if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.lob.com/v1/addresses", nil)
if err != nil {
continue
}
req.SetBasicAuth(resMatch, "")
res, err := client.Do(req)
if err == nil {
defer func() { _ = res.Body.Close() }()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
verified, err := s.verify(ctx, resMatch)
s1.Verified = verified
s1.SetVerificationError(err)
}

results = append(results, s1)
Expand All @@ -66,6 +73,30 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
return results, nil
}

func (s Scanner) verify(ctx context.Context, key string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.lob.com/v1/us_verifications", nil)
if err != nil {
return false, err
}
req.SetBasicAuth(key, "")
client := s.getClient()
res, err := client.Do(req)
if err != nil {
return false, err
}
defer func() { _ = res.Body.Close() }()
switch res.StatusCode {
case http.StatusForbidden, http.StatusUnprocessableEntity:
// 403 indicates key is active but no billing method on file
// 422 indicates key is active but request body is invalid
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
Comment thread
shahzadhaider1 marked this conversation as resolved.
}

func (s Scanner) Type() detector_typepb.DetectorType {
return detector_typepb.DetectorType_Lob
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/detectors/lob/lob_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ func TestLob_FromChunk(t *testing.T) {
{
DetectorType: detector_typepb.DetectorType_Lob,
Verified: true,
ExtraData: map[string]string{
"environment": "live",
},
},
},
wantErr: false,
Expand All @@ -66,6 +69,9 @@ func TestLob_FromChunk(t *testing.T) {
{
DetectorType: detector_typepb.DetectorType_Lob,
Verified: false,
ExtraData: map[string]string{
"environment": "live",
},
},
},
wantErr: false,
Expand Down Expand Up @@ -95,6 +101,10 @@ func TestLob_FromChunk(t *testing.T) {
t.Fatalf("no raw secret present: \n %+v", got[i])
}
got[i].Raw = nil
if len(got[i].SecretParts) == 0 {
t.Fatalf("no secret parts present: \n %+v", got[i])
}
got[i].SecretParts = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Lob.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
Expand Down
24 changes: 12 additions & 12 deletions pkg/detectors/lob/lob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import (
)

var (
validPattern = "XUTbwiuF1_qmP5BvXi4hMeDafM2VoNz5yMH__rI5"
invalidPattern = "XUTbwiuF1_qmP5BvXi4hMeDafM2VoNz5yMH__rI"
keyword = "lob"
validPattern = "live_0979969b3f6cc23ed67e9b650bfaf64f710"
validPatternTest = "test_0979969b3f6cc23ed67e9b650bfaf64f710"
invalidPattern = "live_0979969b3f6cc23ed67e9b650bfaf64f71"
)

func TestLob_Pattern(t *testing.T) {
Expand All @@ -26,23 +26,23 @@ func TestLob_Pattern(t *testing.T) {
want []string
}{
{
name: "valid pattern - with keyword lob",
input: fmt.Sprintf("%s token = '%s'", keyword, validPattern),
name: "valid live pattern",
input: fmt.Sprintf("token = '%s'", validPattern),
want: []string{validPattern},
},
{
name: "valid pattern - ignore duplicate",
input: fmt.Sprintf("%s token = '%s' | '%s'", keyword, validPattern, validPattern),
want: []string{validPattern},
name: "valid test pattern",
input: fmt.Sprintf("token = '%s'", validPatternTest),
want: []string{validPatternTest},
},
{
name: "valid pattern - key out of prefix range",
input: fmt.Sprintf("%s keyword is not close to the real key in the data\n = '%s'", keyword, validPattern),
want: []string{},
name: "valid pattern - ignore duplicate",
input: fmt.Sprintf("token = '%s' | '%s'", validPattern, validPattern),
want: []string{validPattern},
},
{
name: "invalid pattern",
input: fmt.Sprintf("%s = '%s'", keyword, invalidPattern),
input: fmt.Sprintf("'%s'", invalidPattern),
want: []string{},
},
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/engine/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/liveagent"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/livestorm"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/loadmill"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/lob"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/locationiq"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/loggly"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/loginradius"
Expand Down Expand Up @@ -1327,6 +1328,7 @@ func buildDetectorList() []detectors.Detector {
&liveagent.Scanner{},
&livestorm.Scanner{},
&loadmill.Scanner{},
&lob.Scanner{},
&locationiq.Scanner{},
&loggly.Scanner{},
&loginradius.Scanner{},
Expand Down
1 change: 0 additions & 1 deletion pkg/engine/defaults/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ var excludedFromDefaultList = map[detector_typepb.DetectorType]struct{}{
detector_typepb.DetectorType_DatadogApikey: {},
detector_typepb.DetectorType_Guru: {},
detector_typepb.DetectorType_IPInfo: {},
detector_typepb.DetectorType_Lob: {},
detector_typepb.DetectorType_Rev: {},
detector_typepb.DetectorType_TLy: {},
detector_typepb.DetectorType_Tru: {},
Expand Down
Loading