diff --git a/pkg/detectors/ngrok/ngrok.go b/pkg/detectors/ngrok/ngrok.go index 0ad7063f5..bd2219876 100644 --- a/pkg/detectors/ngrok/ngrok.go +++ b/pkg/detectors/ngrok/ngrok.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "strings" regexp "github.com/wasilibs/go-re2" @@ -31,6 +32,11 @@ func (s Scanner) Keywords() []string { return []string{"ngrok"} } +const ( + ngrokVerificationURL = "https://api.ngrok.com/agent_ingresses" + tunnelCredentialErrorCode = "ERR_NGROK_206" +) + var ( keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ngrok"}) + `\b(2[a-zA-Z0-9]{26}_\d[a-zA-Z0-9]{20})\b`) ) @@ -69,7 +75,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.ngrok.com/agent_ingresses", nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, ngrokVerificationURL, nil) if err != nil { return false, err } @@ -90,7 +96,16 @@ func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, return true, nil case http.StatusUnauthorized: return false, nil - default: - return false, fmt.Errorf("ngrok: unexpected status code: %d", res.StatusCode) + case http.StatusBadRequest: + bodyBytes, err := io.ReadAll(res.Body) + if err != nil { + return false, err + } + // Check if the error code is "ERR_NGROK_206" which indicates that + // the credential is a valid tunnel Authtoken rather than an API key. + if strings.Contains(string(bodyBytes), tunnelCredentialErrorCode) { + return true, nil + } } + return false, fmt.Errorf("ngrok: unexpected status code: %d", res.StatusCode) } diff --git a/pkg/detectors/ngrok/ngrok_integration_test.go b/pkg/detectors/ngrok/ngrok_integration_test.go index 234785e80..4882b2110 100644 --- a/pkg/detectors/ngrok/ngrok_integration_test.go +++ b/pkg/detectors/ngrok/ngrok_integration_test.go @@ -25,7 +25,8 @@ func TestNgrok_FromChunk(t *testing.T) { if err != nil { t.Fatalf("could not get test secrets from GCP: %s", err) } - secret := testSecrets.MustGetField("NGROK") + secretAPIKey := testSecrets.MustGetField("NGROK") + secretAuthtoken := testSecrets.MustGetField("NGROK_AUTHTOKEN") inactiveSecret := testSecrets.MustGetField("NGROK_INACTIVE") type args struct { @@ -42,11 +43,28 @@ func TestNgrok_FromChunk(t *testing.T) { wantVerificationErr bool }{ { - name: "found, verified", + name: "found, API key, verified", s: Scanner{}, args: args{ ctx: context.Background(), - data: []byte(fmt.Sprintf("You can find a ngrok secret %s within", secret)), + data: []byte(fmt.Sprintf("You can find a ngrok secret API key %s within", secretAPIKey)), + verify: true, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_Ngrok, + Verified: true, + }, + }, + wantErr: false, + wantVerificationErr: false, + }, + { + name: "found, tunnel authtoken, verified", + s: Scanner{}, + args: args{ + ctx: context.Background(), + data: []byte(fmt.Sprintf("You can find a ngrok secret tunnel authtoken %s within", secretAuthtoken)), verify: true, }, want: []detectors.Result{