-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat: add MercadoPago credentials detector #4792
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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), | ||
| } | ||
|
|
||
| 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
|
||
| 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." | ||
| } | ||
| 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
|
||
|
|
||
| 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) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1052,6 +1052,7 @@ enum DetectorType { | |
| OpenAIAdmin = 1040; | ||
| GoogleGeminiAPIKey = 1041; | ||
| ArtifactoryReferenceToken = 1042; | ||
| MercadoPago = 1043; | ||
| } | ||
|
Comment on lines
1052
to
1056
|
||
|
|
||
| message Result { | ||
|
|
||
There was a problem hiding this comment.
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.