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

import (
"context"
"strings"

regexp "github.com/wasilibs/go-re2"

"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
)

type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}

var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)

var (
// Mailgun webhook signing tokens are commonly represented as 32 hex characters.
// Require "mailgun" in nearby context to reduce noise from generic webhook examples.
tokenPat = regexp.MustCompile(detectors.PrefixRegex([]string{"mailgun"}) + `\b([a-fA-F0-9]{32})(?:['"|\n\r\s\x60;]|$)`)
)

func (s Scanner) Keywords() []string {
return []string{"mailgun", "webhook", "signing"}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overly broad keywords cause unnecessary detector invocations

Medium Severity

Keywords() returns ["mailgun", "webhook", "signing"] but the regex (via PrefixRegex) requires "mailgun" to be present in the data for any match. The keywords "webhook" and "signing" are extremely common words that will trigger this detector on many irrelevant chunks where no "mailgun" context exists, causing unnecessary regex processing that can never produce results. Only "mailgun" is needed as a keyword.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c3e3f96. Configure here.


func (s Scanner) Type() detector_typepb.DetectorType {
return detector_typepb.DetectorType_MailgunWebhookToken
}

func (s Scanner) Description() string {
return "Mailgun webhook tokens are used to verify webhook payload signatures."
}

func (s Scanner) FromData(_ context.Context, _ bool, data []byte) ([]detectors.Result, error) {
dataStr := string(data)

uniqueMatches := make(map[string]struct{})
for _, match := range tokenPat.FindAllStringSubmatch(dataStr, -1) {
if len(match) < 2 {
continue
}
uniqueMatches[strings.TrimSpace(match[1])] = struct{}{}
}

results := make([]detectors.Result, 0, len(uniqueMatches))
for token := range uniqueMatches {
results = append(results, detectors.Result{
DetectorType: detector_typepb.DetectorType_MailgunWebhookToken,
Raw: []byte(token),
ExtraData: map[string]string{
"rotation_guide": "https://help.mailgun.com/hc/en-us/articles/360018328934-How-can-I-verify-webhooks",
},
SecretParts: map[string]string{"token": token},
})
}

return results, nil
}

func (s Scanner) IsFalsePositive(result detectors.Result) (bool, string) {
return detectors.IsKnownFalsePositive(string(result.Raw), detectors.DefaultFalsePositives, true)
}
112 changes: 112 additions & 0 deletions pkg/detectors/mailgunwebhooktoken/mailgun_webhook_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package mailgunwebhooktoken

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"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
)

func TestMailgunWebhookToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})

tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern - webhook signing assignment",
input: `MAILGUN_WEBHOOK_SIGNING_KEY="9f86d081884c7d659a2feaa0c55ad015"`,
want: []string{"9f86d081884c7d659a2feaa0c55ad015"},
},
{
name: "valid pattern - uppercase hex",
input: `mailgun webhook token = "0123456789ABCDEF0123456789ABCDEF"`,
want: []string{"0123456789ABCDEF0123456789ABCDEF"},
},
{
name: "valid pattern - multiple tokens",
input: `mailgun primary webhook signing key=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
mailgun backup webhook signing key=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb`,
want: []string{
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
},
},
{
name: "deduplication - repeated token",
input: `mailgun webhook signing token cccccccccccccccccccccccccccccccc mailgun webhook signing token cccccccccccccccccccccccccccccccc`,
want: []string{"cccccccccccccccccccccccccccccccc"},
},
{
name: "invalid pattern - too short",
input: `mailgun webhook signing key = "abc123"`,
want: nil,
},
{
name: "invalid pattern - non-hex characters",
input: `mailgun webhook signing key = "gggggggggggggggggggggggggggggggg"`,
want: nil,
},
{
name: "invalid pattern - missing mailgun context",
input: `webhook signing key = "dddddddddddddddddddddddddddddddd"`,
want: nil,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(test.want) > 0 && len(matchedDetectors) == 0 {
t.Errorf("keywords %v not found in input", 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("expected %d results, 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)
}
})
}
}

func TestMailgunWebhookToken_Type(t *testing.T) {
s := Scanner{}
require.Equal(t, detector_typepb.DetectorType_MailgunWebhookToken, s.Type())
}

func TestMailgunWebhookToken_Keywords(t *testing.T) {
s := Scanner{}
require.NotEmpty(t, s.Keywords())
require.Contains(t, s.Keywords(), "mailgun")
require.Contains(t, s.Keywords(), "webhook")
require.Contains(t, s.Keywords(), "signing")
}
2 changes: 2 additions & 0 deletions pkg/engine/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/mailchimp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/mailerlite"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/mailgun"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/mailgunwebhooktoken"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/mailjetbasicauth"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/mailjetsms"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/mailmodo"
Expand Down Expand Up @@ -1346,6 +1347,7 @@ func buildDetectorList() []detectors.Detector {
&mailchimp.Scanner{},
&mailerlite.Scanner{},
&mailgun.Scanner{},
&mailgunwebhooktoken.Scanner{},
&mailjetbasicauth.Scanner{},
&mailjetsms.Scanner{},
&mailmodo.Scanner{},
Expand Down
3 changes: 3 additions & 0 deletions pkg/pb/detector_typepb/detector_type.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions proto/detector_type.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1054,4 +1054,5 @@ enum DetectorType {
GitLabOauth2 = 1050;
SpectralOps = 1051;
AWSAppSync = 1052;
MailgunWebhookToken = 1053;
}