diff --git a/pkg/detectors/ngrok/ngrok.go b/pkg/detectors/ngrok/ngrok.go index ede1b6eb8..0ff517fc0 100644 --- a/pkg/detectors/ngrok/ngrok.go +++ b/pkg/detectors/ngrok/ngrok.go @@ -38,7 +38,13 @@ const ( ) var ( - keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ngrok"}) + `\b(2[a-zA-Z0-9]{26}_\d[a-zA-Z0-9]{20})\b`) + // ngrok API keys and authtokens are {prefix}_{suffix}, both alphanumeric with + // no fixed leading character. The prefix is 27 chars. Suffixes are usually 21 + // chars with a leading digit, but real authtokens verified against the ngrok + // API (via ERR_NGROK_206) have been observed with 20-char and letter-leading + // suffixes, so the pattern must accept 20-21 chars starting with any + // alphanumeric character. + keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ngrok"}) + `\b([a-zA-Z0-9]{27}_[a-zA-Z0-9]{20,21})\b`) ) func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { diff --git a/pkg/detectors/ngrok/ngrok_test.go b/pkg/detectors/ngrok/ngrok_test.go index 033b56ed1..71d3da08f 100644 --- a/pkg/detectors/ngrok/ngrok_test.go +++ b/pkg/detectors/ngrok/ngrok_test.go @@ -12,9 +12,15 @@ import ( ) var ( - validPattern = "2WIRSIHOQyHSVklZnoz2k6bTYdH_0E7z0Ta9QEyR1fZvQ0KU9" - invalidPattern = "2WIRSIHOQyHSVklZnoz2k6bT?dH_0E7z0Ta9QEyR1fZvQ0KU9" - keyword = "ngrok" + validPattern = "2WIRSIHOQyHSVklZnoz2k6bTYdH_0E7z0Ta9QEyR1fZvQ0KU9" + validPatternAlt = "3GuTI5JXfaLtYILkc6N0xopP2V0_89zkDfefiuMv6Hk1z6oYi" + // Authtokens exist in the wild with letter-leading and 20-char suffixes + // (confirmed live against the ngrok API), so both must be detected. + validPatternLetterSuffix = "3GuTI5JXfaLtYILkc6N0xopP2V0_a9zkDfefiuMv6Hk1z6oYi" + validPatternShortSuffix = "4HvUJ6KYgbMuZJMld7O1yqqQ3W1_b8ylEgfgjvNw7Il2z7pZ" + invalidPattern = "2WIRSIHOQyHSVklZnoz2k6bT?dH_0E7z0Ta9QEyR1fZvQ0KU9" + invalidIDPattern = "ak_3GuTI5JXfaLtYILkc6N0xopP2V0" + keyword = "ngrok" ) func TestNgrok_Pattern(t *testing.T) { @@ -30,11 +36,26 @@ func TestNgrok_Pattern(t *testing.T) { input: fmt.Sprintf("%s token = '%s'", keyword, validPattern), want: []string{"2WIRSIHOQyHSVklZnoz2k6bTYdH_0E7z0Ta9QEyR1fZvQ0KU9"}, }, + { + name: "valid pattern - api key not starting with 2", + input: fmt.Sprintf("%s token = '%s'", keyword, validPatternAlt), + want: []string{validPatternAlt}, + }, { name: "valid pattern - ignore duplicate", input: fmt.Sprintf("%s token = '%s' | '%s'", keyword, validPattern, validPattern), want: []string{"2WIRSIHOQyHSVklZnoz2k6bTYdH_0E7z0Ta9QEyR1fZvQ0KU9"}, }, + { + name: "valid pattern - authtoken suffix not starting with digit", + input: fmt.Sprintf("%s token = '%s'", keyword, validPatternLetterSuffix), + want: []string{validPatternLetterSuffix}, + }, + { + name: "valid pattern - authtoken with 20 char suffix", + input: fmt.Sprintf("%s token = '%s'", keyword, validPatternShortSuffix), + want: []string{validPatternShortSuffix}, + }, { 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), @@ -45,6 +66,11 @@ func TestNgrok_Pattern(t *testing.T) { input: fmt.Sprintf("%s = '%s'", keyword, invalidPattern), want: []string{}, }, + { + name: "invalid pattern - api key resource id not bearer token", + input: fmt.Sprintf("%s api_key = '%s'", keyword, invalidIDPattern), + want: []string{}, + }, } for _, test := range tests {