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
104 changes: 104 additions & 0 deletions pkg/detectors/mercadopago/mercadopago.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package mercadopago

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

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

type Scanner struct {
client *http.Client
}

const verifyURL = "https://api.mercadopago.com/v1/payments/search?sort=date_created&criteria=desc&range=date_created&begin_date=NOW-1DAYS&end_date=NOW"

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

defaultClient = common.SaneHttpClient()

// MercadoPago access token format: APP_USR-{16digits}-{6digits}-{32hex}__LA_LD__-{digits}
keyPat = regexp.MustCompile(`\b(APP_USR-\d{16}-\d{6}-[a-f0-9]{32}__[A-Z]{2}_[A-Z]{2}__-\d+)\b`)
)

// 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{"APP_USR-"}
}

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

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

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

for _, match := range matches {
resMatch := strings.TrimSpace(match[1])

s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_MercadoPago,
Raw: []byte(resMatch),
Comment on lines +55 to +57
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

This references detectorspb.DetectorType_MercadoPago, but the currently committed generated protobuf file (pkg/pb/detectorspb/detectors.pb.go) does not include that enum value yet. The PR needs to include regenerated protobuf outputs (or otherwise ensure they are generated in CI) or this detector won’t compile.

Copilot uses AI. Check for mistakes.
}

if verify {
client := s.getClient()
isVerified, verificationErr := verifyMercadoPago(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}

results = append(results, s1)
}

return results, nil
}

func verifyMercadoPago(ctx context.Context, client *http.Client, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, verifyURL, nil)
if err != nil {
return false, err
}
req.Header.Add("Authorization", "Bearer "+token)
res, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
res.Body.Close()
}()

Comment on lines +79 to +87
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The verification HTTP response body is closed but not drained. In this codebase, verifier helpers typically io.Copy(io.Discard, resp.Body) before closing to allow connection reuse and avoid leaking resources under load. Consider adopting the same defer pattern used in other detectors (e.g., meraki) here.

Copilot uses AI. Check for mistakes.
switch res.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized, http.StatusForbidden:
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}

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

func (s Scanner) Description() string {
return "MercadoPago is a leading payment processing platform in Latin America. MercadoPago access tokens can be used to process payments, view transaction history, and manage merchant accounts."
}
83 changes: 83 additions & 0 deletions pkg/detectors/mercadopago/mercadopago_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package mercadopago

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 TestMercadoPago_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})

tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern - access token",
input: "mercadopago_token = APP_USR-1234567890123456-010122-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6__LA_LD__-987654321",
want: []string{"APP_USR-1234567890123456-010122-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6__LA_LD__-987654321"},
},
{
name: "valid pattern - in env file",
input: "MERCADO_PAGO_ACCESS_TOKEN=APP_USR-6329847201583746-031522-f0e1d2c3b4a5968778695a4b3c2d1e0f__LA_LD__-123456789",
want: []string{"APP_USR-6329847201583746-031522-f0e1d2c3b4a5968778695a4b3c2d1e0f__LA_LD__-123456789"},
},
{
name: "invalid pattern - public key format",
input: "APP_USR-12345678-1234-1234-1234-123456789abc",
want: nil,
},
{
name: "invalid pattern - no prefix",
input: "some_random_token_value_that_does_not_match",
want: nil,
},
}
Comment on lines +18 to +43
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

PR description/test plan mentions “5 test cases”, but this table defines 4 cases. Either add the missing case or update the PR description so it matches what’s actually covered by tests.

Copilot uses AI. Check for mistakes.

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
if len(test.want) == 0 {
return
}
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)
}
})
}
}
2 changes: 2 additions & 0 deletions pkg/engine/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/mediastack"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/meistertask"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/meraki"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/mercadopago"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/mesibo"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/messagebird"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/metaapi"
Expand Down Expand Up @@ -1344,6 +1345,7 @@ func buildDetectorList() []detectors.Detector {
&mediastack.Scanner{},
&meistertask.Scanner{},
&meraki.Scanner{},
&mercadopago.Scanner{},
&mesibo.Scanner{},
&messagebird.Scanner{},
&metaapi.Scanner{},
Expand Down
1 change: 1 addition & 0 deletions proto/detectors.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,7 @@ enum DetectorType {
OpenAIAdmin = 1040;
GoogleGeminiAPIKey = 1041;
ArtifactoryReferenceToken = 1042;
MercadoPago = 1043;
}
Comment on lines 1052 to 1056
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

This new enum value requires regenerating and committing the generated Go protobuf bindings (e.g., pkg/pb/detectorspb/detectors.pb.go). As-is, the repo still only contains values up to ArtifactoryReferenceToken=1042, so references to DetectorType_MercadoPago will not compile until the generated files are updated in the PR (or CI reliably runs protos generation).

Copilot uses AI. Check for mistakes.

message Result {
Expand Down