From 75fa915c42fcfc02b29567dd7013776c424a6586 Mon Sep 17 00:00:00 2001 From: David Sarkisyan <281478990+srkyn@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:52:39 -0400 Subject: [PATCH] Improve YouTube API key verifier errors --- pkg/detectors/youtubeapikey/youtubeapikey.go | 44 +++++++++++---- .../youtubeapikey/youtubeapikey_test.go | 55 +++++++++++++++++-- 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/pkg/detectors/youtubeapikey/youtubeapikey.go b/pkg/detectors/youtubeapikey/youtubeapikey.go index 3f28f63601ec..a6e89c81b12e 100644 --- a/pkg/detectors/youtubeapikey/youtubeapikey.go +++ b/pkg/detectors/youtubeapikey/youtubeapikey.go @@ -2,6 +2,7 @@ package youtubeapikey import ( "context" + "fmt" "net/http" "strings" @@ -14,6 +15,7 @@ import ( type Scanner struct { detectors.DefaultMultiPartCredentialProvider + client *http.Client } // Ensure the Scanner satisfies the interface at compile time. @@ -33,6 +35,13 @@ func (s Scanner) Keywords() []string { return []string{"youtube"} } +func (s Scanner) getClient() *http.Client { + if s.client != nil { + return s.client + } + return client +} + // FromData will find and optionally verify YoutubeApiKey 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) @@ -53,17 +62,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://www.googleapis.com/youtube/v3/channelSections?key="+resMatch+"&channelId="+resIdmatch, nil) - if err != nil { - continue - } - res, err := client.Do(req) - if err == nil { - defer func() { _ = res.Body.Close() }() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + isVerified, verificationErr := verifyYoutubeAPIKey(ctx, s.getClient(), resMatch, resIdmatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -74,6 +75,27 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +func verifyYoutubeAPIKey(ctx context.Context, client *http.Client, key, channelID string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.googleapis.com/youtube/v3/channelSections?key="+key+"&channelId="+channelID, nil) + if err != nil { + return false, err + } + res, err := client.Do(req) + if err != nil { + return false, err + } + defer func() { _ = res.Body.Close() }() + + switch res.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden: + return false, nil + default: + return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) + } +} + func (s Scanner) Type() detector_typepb.DetectorType { return detector_typepb.DetectorType_YoutubeApiKey } diff --git a/pkg/detectors/youtubeapikey/youtubeapikey_test.go b/pkg/detectors/youtubeapikey/youtubeapikey_test.go index da6a255c190c..aaa7d20bb7af 100644 --- a/pkg/detectors/youtubeapikey/youtubeapikey_test.go +++ b/pkg/detectors/youtubeapikey/youtubeapikey_test.go @@ -3,20 +3,23 @@ package youtubeapikey import ( "context" "fmt" + "net/http" "testing" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick" ) var ( - validKey = "8SN8OtkzJ6z2tcFtv93Gl63o97LGGBvYAyJviDg" - invalidKey = "8SN8OtkzJ6z2tcFtv93?l63o97LGGBvYAyJviDg" - validId = "0ifNmkGT6biPToj9TDGYqyFP" - invalidId = "0ifNmkG?6biPToj9TDGYqyFP" - keyword = "youtubeapikey" + validKey = "8SN8OtkzJ6z2tcFtv93Gl63o97LGGBvYAyJviDg" + invalidKey = "8SN8OtkzJ6z2tcFtv93?l63o97LGGBvYAyJviDg" + validId = "0ifNmkGT6biPToj9TDGYqyFP" + invalidId = "0ifNmkG?6biPToj9TDGYqyFP" + keyword = "youtubeapikey" ) func TestYoutubeApiKey_Pattern(t *testing.T) { @@ -81,3 +84,45 @@ func TestYoutubeApiKey_Pattern(t *testing.T) { }) } } + +func TestYoutubeApiKey_Verification(t *testing.T) { + tests := []struct { + name string + statusCode int + wantVerified bool + wantVerificationErr string + }{ + { + name: "verified", + statusCode: http.StatusOK, + wantVerified: true, + }, + { + name: "unverified", + statusCode: http.StatusBadRequest, + }, + { + name: "verification error", + statusCode: http.StatusInternalServerError, + wantVerificationErr: "unexpected HTTP response status 500", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + d := Scanner{client: common.ConstantResponseHttpClient(test.statusCode, "{}")} + input := fmt.Sprintf("%s token - '%s'\n%s token - '%s'\n", keyword, validKey, keyword, validId) + + results, err := d.FromData(context.Background(), true, []byte(input)) + require.NoError(t, err) + require.Len(t, results, 1) + require.Equal(t, test.wantVerified, results[0].Verified) + + if test.wantVerificationErr == "" { + require.NoError(t, results[0].VerificationError()) + } else { + require.EqualError(t, results[0].VerificationError(), test.wantVerificationErr) + } + }) + } +}