From 43297198af59c43438480c43e663e19a6e6c168f Mon Sep 17 00:00:00 2001 From: Nicholas Comer Date: Wed, 8 Apr 2026 12:34:16 +0100 Subject: [PATCH] Update Cloudflare detectors for 2026+ prefixed credential formats ## Context/Background Cloudflare is rolling out new prefixed credential formats in 2026. The new formats (`cfk_`, `cfut_`, `cfat_`) are self-identifying via prefix and do not need keyword proximity matching. Per project convention, new token formats are added via the `Versioner` interface with a `v1`/`v2` directory split. Additionally, CA keys (Service Keys) are now deprecated. ## Changes in this commit - cloudflareglobalapikey: split into `v1`/`v2` with `Versioner` interface. v1 fixes legacy regex to `[a-f0-9]{37,45}` (lowercase hex). v2 adds `cfk_` prefixed format detection. - cloudflareapitoken: split into `v1`/`v2` with `Versioner` interface. v2 adds `cfut_`/`cfat_` prefixed format detection. `cfat_` (account tokens) route verification through the account-scoped `/accounts/:id/tokens/verify` endpoint, extracting account IDs from surrounding data. - cloudflarecakey: add deprecation notice with changelog link. - defaults.go: updated imports and registrations for versioned scanners. --- .../{ => v1}/cloudflareapitoken.go | 35 +++--- .../cloudflareapitoken_integration_test.go | 0 .../{ => v1}/cloudflareapitoken_test.go | 0 .../v2/cloudflareapitoken.go | 117 ++++++++++++++++++ .../v2/cloudflareapitoken_test.go | 80 ++++++++++++ .../cloudflarecakey/cloudflarecakey.go | 5 +- .../{ => v1}/cloudflareglobalapikey.go | 42 ++++--- ...cloudflareglobalapikey_integration_test.go | 0 .../{ => v1}/cloudflareglobalapikey_test.go | 6 +- .../v2/cloudflareglobalapikey.go | 87 +++++++++++++ .../v2/cloudflareglobalapikey_test.go | 81 ++++++++++++ pkg/engine/defaults/defaults.go | 12 +- 12 files changed, 426 insertions(+), 39 deletions(-) rename pkg/detectors/cloudflareapitoken/{ => v1}/cloudflareapitoken.go (69%) rename pkg/detectors/cloudflareapitoken/{ => v1}/cloudflareapitoken_integration_test.go (100%) rename pkg/detectors/cloudflareapitoken/{ => v1}/cloudflareapitoken_test.go (100%) create mode 100644 pkg/detectors/cloudflareapitoken/v2/cloudflareapitoken.go create mode 100644 pkg/detectors/cloudflareapitoken/v2/cloudflareapitoken_test.go rename pkg/detectors/cloudflareglobalapikey/{ => v1}/cloudflareglobalapikey.go (69%) rename pkg/detectors/cloudflareglobalapikey/{ => v1}/cloudflareglobalapikey_integration_test.go (100%) rename pkg/detectors/cloudflareglobalapikey/{ => v1}/cloudflareglobalapikey_test.go (88%) create mode 100644 pkg/detectors/cloudflareglobalapikey/v2/cloudflareglobalapikey.go create mode 100644 pkg/detectors/cloudflareglobalapikey/v2/cloudflareglobalapikey_test.go diff --git a/pkg/detectors/cloudflareapitoken/cloudflareapitoken.go b/pkg/detectors/cloudflareapitoken/v1/cloudflareapitoken.go similarity index 69% rename from pkg/detectors/cloudflareapitoken/cloudflareapitoken.go rename to pkg/detectors/cloudflareapitoken/v1/cloudflareapitoken.go index fd712bddbf80..5fc00084c6ee 100644 --- a/pkg/detectors/cloudflareapitoken/cloudflareapitoken.go +++ b/pkg/detectors/cloudflareapitoken/v1/cloudflareapitoken.go @@ -15,8 +15,11 @@ import ( type Scanner struct{} -// Ensure the Scanner satisfies the interface at compile time. +// Ensure the Scanner satisfies the interfaces at compile time. var _ detectors.Detector = (*Scanner)(nil) +var _ detectors.Versioner = (*Scanner)(nil) + +func (Scanner) Version() int { return 1 } var ( client = common.SaneHttpClient() @@ -45,19 +48,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloudflare.com/client/v4/user/tokens/verify", nil) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch)) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + s1.Verified = VerifyUserToken(ctx, client, resMatch) } results = append(results, s1) @@ -66,6 +57,22 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +// VerifyUserToken checks if a Cloudflare user API token is valid. +func VerifyUserToken(ctx context.Context, client *http.Client, token string) bool { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloudflare.com/client/v4/user/tokens/verify", nil) + if err != nil { + return false + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + res, err := client.Do(req) + if err != nil { + return false + } + defer res.Body.Close() + return res.StatusCode >= 200 && res.StatusCode < 300 +} + func (s Scanner) Type() detector_typepb.DetectorType { return detector_typepb.DetectorType_CloudflareApiToken } diff --git a/pkg/detectors/cloudflareapitoken/cloudflareapitoken_integration_test.go b/pkg/detectors/cloudflareapitoken/v1/cloudflareapitoken_integration_test.go similarity index 100% rename from pkg/detectors/cloudflareapitoken/cloudflareapitoken_integration_test.go rename to pkg/detectors/cloudflareapitoken/v1/cloudflareapitoken_integration_test.go diff --git a/pkg/detectors/cloudflareapitoken/cloudflareapitoken_test.go b/pkg/detectors/cloudflareapitoken/v1/cloudflareapitoken_test.go similarity index 100% rename from pkg/detectors/cloudflareapitoken/cloudflareapitoken_test.go rename to pkg/detectors/cloudflareapitoken/v1/cloudflareapitoken_test.go diff --git a/pkg/detectors/cloudflareapitoken/v2/cloudflareapitoken.go b/pkg/detectors/cloudflareapitoken/v2/cloudflareapitoken.go new file mode 100644 index 000000000000..db78e233c0c6 --- /dev/null +++ b/pkg/detectors/cloudflareapitoken/v2/cloudflareapitoken.go @@ -0,0 +1,117 @@ +package cloudflareapitoken + +import ( + "context" + "fmt" + "net/http" + "strings" + + regexp "github.com/wasilibs/go-re2" + + "github.com/trufflesecurity/trufflehog/v3/pkg/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" + v1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudflareapitoken/v1" + "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb" +) + +type Scanner struct { + v1.Scanner +} + +// Ensure the Scanner satisfies the interfaces at compile time. +var _ detectors.Detector = (*Scanner)(nil) +var _ detectors.Versioner = (*Scanner)(nil) + +func (Scanner) Version() int { return 2 } + +var ( + client = common.SaneHttpClient() + + // 2026+ formats: cfut_ (user token) and cfat_ (account token), self-identifying. + keyV2Pat = regexp.MustCompile(`\b(cf[ua]t_[a-zA-Z0-9]{40}[a-f0-9]{8})\b`) + // Cloudflare account ID pattern for cfat_ token verification. + accountIDPat = regexp.MustCompile(`\b([a-f0-9]{32})\b`) +) + +// Keywords are used for efficiently pre-filtering chunks. +func (s Scanner) Keywords() []string { + return []string{"cfut_", "cfat_"} +} + +// FromData will find and optionally verify CloudflareApiToken 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 := keyV2Pat.FindAllStringSubmatch(dataStr, -1) + + // Extract account IDs from surrounding data for cfat_ verification. + uniqueAccountIDs := make(map[string]struct{}) + for _, match := range accountIDPat.FindAllStringSubmatch(dataStr, -1) { + uniqueAccountIDs[match[1]] = struct{}{} + } + + for _, match := range matches { + resMatch := strings.TrimSpace(match[1]) + + if verify { + if strings.HasPrefix(resMatch, "cfat_") { + // Account tokens require per-account verification. + for accountID := range uniqueAccountIDs { + s1 := detectors.Result{ + DetectorType: detector_typepb.DetectorType_CloudflareApiToken, + Raw: []byte(resMatch), + RawV2: []byte(resMatch + accountID), + } + s1.Verified = verifyAccountToken(ctx, resMatch, accountID) + results = append(results, s1) + } + if len(uniqueAccountIDs) == 0 { + // No account ID found; still report the token unverified. + results = append(results, detectors.Result{ + DetectorType: detector_typepb.DetectorType_CloudflareApiToken, + Raw: []byte(resMatch), + }) + } + } else { + // cfut_ tokens use the user token verification endpoint. + s1 := detectors.Result{ + DetectorType: detector_typepb.DetectorType_CloudflareApiToken, + Raw: []byte(resMatch), + } + s1.Verified = v1.VerifyUserToken(ctx, client, resMatch) + results = append(results, s1) + } + } else { + results = append(results, detectors.Result{ + DetectorType: detector_typepb.DetectorType_CloudflareApiToken, + Raw: []byte(resMatch), + }) + } + } + + return results, nil +} + +func verifyAccountToken(ctx context.Context, token, accountID string) bool { + url := fmt.Sprintf("https://api.cloudflare.com/client/v4/accounts/%s/tokens/verify", accountID) + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return false + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + res, err := client.Do(req) + if err != nil { + return false + } + defer res.Body.Close() + return res.StatusCode >= 200 && res.StatusCode < 300 +} + +func (s Scanner) Type() detector_typepb.DetectorType { + return detector_typepb.DetectorType_CloudflareApiToken +} + +func (s Scanner) Description() string { + return "Cloudflare is a web infrastructure and website security company. Cloudflare API tokens (cfut_/cfat_ prefixed, 2026+ format) can be used to manage and interact with Cloudflare services." +} diff --git a/pkg/detectors/cloudflareapitoken/v2/cloudflareapitoken_test.go b/pkg/detectors/cloudflareapitoken/v2/cloudflareapitoken_test.go new file mode 100644 index 000000000000..064df352cb5b --- /dev/null +++ b/pkg/detectors/cloudflareapitoken/v2/cloudflareapitoken_test.go @@ -0,0 +1,80 @@ +package cloudflareapitoken + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" + "github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick" +) + +func TestCloudFlareAPITokenV2_Pattern(t *testing.T) { + d := Scanner{} + ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d}) + + tests := []struct { + name string + input string + want []string + }{ + { + name: "valid v2 user token - no keyword proximity needed", + input: "token: cfut_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfe", + want: []string{"cfut_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfe"}, + }, + { + name: "valid v2 account token - no keyword proximity needed", + input: "token: cfat_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfe", + want: []string{"cfat_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfe"}, + }, + { + name: "no match for legacy format", + input: "cfut_: kOjD1yceduu2jxL2uuwT9dkOIudU3_54sLCEud6j", + want: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input)) + if len(matchedDetectors) == 0 && test.want != nil { + t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input) + return + } + + results, err := d.FromData(context.Background(), false, []byte(test.input)) + if err != nil { + t.Errorf("error = %v", err) + return + } + + if len(results) != len(test.want) { + if len(results) == 0 { + t.Errorf("did not receive result") + } else { + t.Errorf("expected %d results, only received %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) + } + }) + } +} diff --git a/pkg/detectors/cloudflarecakey/cloudflarecakey.go b/pkg/detectors/cloudflarecakey/cloudflarecakey.go index 31ed93b13030..267170544d90 100644 --- a/pkg/detectors/cloudflarecakey/cloudflarecakey.go +++ b/pkg/detectors/cloudflarecakey/cloudflarecakey.go @@ -21,7 +21,10 @@ var _ detectors.Detector = (*Scanner)(nil) var ( client = common.SaneHttpClient() - // origin ca keys documentation: https://developers.cloudflare.com/fundamentals/api/get-started/ca-keys/ + // Origin CA keys (aka "Service Keys") are deprecated as of 2026-03-19: + // https://developers.cloudflare.com/changelog/post/2026-03-19-service-key-authentication-deprecated/ + // + // Reference: https://developers.cloudflare.com/fundamentals/api/get-started/ca-keys/ keyPat = regexp.MustCompile(`\b(v1\.0-[A-Za-z0-9-]{171})\b`) ) diff --git a/pkg/detectors/cloudflareglobalapikey/cloudflareglobalapikey.go b/pkg/detectors/cloudflareglobalapikey/v1/cloudflareglobalapikey.go similarity index 69% rename from pkg/detectors/cloudflareglobalapikey/cloudflareglobalapikey.go rename to pkg/detectors/cloudflareglobalapikey/v1/cloudflareglobalapikey.go index 66d14f4d96dd..9fee319df9b7 100644 --- a/pkg/detectors/cloudflareglobalapikey/cloudflareglobalapikey.go +++ b/pkg/detectors/cloudflareglobalapikey/v1/cloudflareglobalapikey.go @@ -16,13 +16,17 @@ type Scanner struct { detectors.DefaultMultiPartCredentialProvider } -// Ensure the Scanner satisfies the interface at compile time. +// Ensure the Scanner satisfies the interfaces at compile time. var _ detectors.Detector = (*Scanner)(nil) +var _ detectors.Versioner = (*Scanner)(nil) + +func (Scanner) Version() int { return 1 } var ( client = common.SaneHttpClient() - apiKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloudflare"}) + `\b([A-Za-z0-9_-]{37})\b`) + // Pre-2026 format: lowercase hex, 37-45 chars, requires "cloudflare" keyword nearby. + apiKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cloudflare"}) + `\b([a-f0-9]{37,45})\b`) emailPat = regexp.MustCompile(common.EmailPattern) ) @@ -56,21 +60,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloudflare.com/client/v4/user", nil) - if err != nil { - continue - } - req.Header.Add("X-Auth-Email", emailMatch) - req.Header.Add("X-Auth-Key", apiKeyRes) - req.Header.Add("Content-Type", "application/json") - - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + s1.Verified = VerifyGlobalAPIKey(ctx, client, apiKeyRes, emailMatch) } results = append(results, s1) @@ -80,6 +70,24 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +// VerifyGlobalAPIKey checks if a Cloudflare Global API Key is valid. +func VerifyGlobalAPIKey(ctx context.Context, client *http.Client, apiKey, email string) bool { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloudflare.com/client/v4/user", nil) + if err != nil { + return false + } + req.Header.Add("X-Auth-Email", email) + req.Header.Add("X-Auth-Key", apiKey) + req.Header.Add("Content-Type", "application/json") + + res, err := client.Do(req) + if err != nil { + return false + } + defer res.Body.Close() + return res.StatusCode >= 200 && res.StatusCode < 300 +} + func (s Scanner) Type() detector_typepb.DetectorType { return detector_typepb.DetectorType_CloudflareGlobalApiKey } diff --git a/pkg/detectors/cloudflareglobalapikey/cloudflareglobalapikey_integration_test.go b/pkg/detectors/cloudflareglobalapikey/v1/cloudflareglobalapikey_integration_test.go similarity index 100% rename from pkg/detectors/cloudflareglobalapikey/cloudflareglobalapikey_integration_test.go rename to pkg/detectors/cloudflareglobalapikey/v1/cloudflareglobalapikey_integration_test.go diff --git a/pkg/detectors/cloudflareglobalapikey/cloudflareglobalapikey_test.go b/pkg/detectors/cloudflareglobalapikey/v1/cloudflareglobalapikey_test.go similarity index 88% rename from pkg/detectors/cloudflareglobalapikey/cloudflareglobalapikey_test.go rename to pkg/detectors/cloudflareglobalapikey/v1/cloudflareglobalapikey_test.go index fef882a30b73..ab849aec66ef 100644 --- a/pkg/detectors/cloudflareglobalapikey/cloudflareglobalapikey_test.go +++ b/pkg/detectors/cloudflareglobalapikey/v1/cloudflareglobalapikey_test.go @@ -12,8 +12,8 @@ import ( ) var ( - validPattern = "abcD123efg456HIJklmn789OPQ_rstUVWxYZ-012 / testuser1005@example.com" - invalidPattern = "abcD123efg456HIJklmn789OPQ_rstUVWxYZ-012/testing@go" + validPattern = "abcdef1234567890abcdef1234567890abcdef0 / testuser1005@example.com" + invalidPattern = "abcdef1234567890abcdef1234567890abcdef0/testing@go" ) func TestCloudFlareGlobalAPIKey_Pattern(t *testing.T) { @@ -28,7 +28,7 @@ func TestCloudFlareGlobalAPIKey_Pattern(t *testing.T) { { name: "valid pattern", input: fmt.Sprintf("cloudflare: %s", validPattern), - want: []string{"abcD123efg456HIJklmn789OPQ_rstUVWxYZ-testuser1005@example.com"}, + want: []string{"abcdef1234567890abcdef1234567890abcdef0testuser1005@example.com"}, }, { name: "valid pattern - key out of prefix range", diff --git a/pkg/detectors/cloudflareglobalapikey/v2/cloudflareglobalapikey.go b/pkg/detectors/cloudflareglobalapikey/v2/cloudflareglobalapikey.go new file mode 100644 index 000000000000..9a780045b08f --- /dev/null +++ b/pkg/detectors/cloudflareglobalapikey/v2/cloudflareglobalapikey.go @@ -0,0 +1,87 @@ +package cloudflareglobalapikey + +import ( + "context" + "strings" + + regexp "github.com/wasilibs/go-re2" + + "github.com/trufflesecurity/trufflehog/v3/pkg/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" + v1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudflareglobalapikey/v1" + "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb" +) + +type Scanner struct { + v1.Scanner +} + +// Ensure the Scanner satisfies the interfaces at compile time. +var _ detectors.Detector = (*Scanner)(nil) +var _ detectors.Versioner = (*Scanner)(nil) + +func (Scanner) Version() int { return 2 } + +var ( + client = common.SaneHttpClient() + + // 2026+ format: cfk_ prefix, sufficiently unique to match without keyword proximity. + apiKeyV2Pat = regexp.MustCompile(`\b(cfk_[a-zA-Z0-9]{40}[a-f0-9]{8})\b`) + + emailPat = regexp.MustCompile(common.EmailPattern) +) + +// Keywords are used for efficiently pre-filtering chunks. +func (s Scanner) Keywords() []string { + return []string{"cfk_"} +} + +// FromData will find and optionally verify CloudflareGlobalApiKey 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) + + apiKeyMatches := apiKeyV2Pat.FindAllStringSubmatch(dataStr, -1) + + uniqueEmailMatches := make(map[string]struct{}) + for _, match := range emailPat.FindAllStringSubmatch(dataStr, -1) { + uniqueEmailMatches[strings.TrimSpace(match[1])] = struct{}{} + } + + for _, apiKeyMatch := range apiKeyMatches { + apiKeyRes := strings.TrimSpace(apiKeyMatch[1]) + + if len(uniqueEmailMatches) == 0 { + // No email found; still report the token unverified. + results = append(results, detectors.Result{ + DetectorType: detector_typepb.DetectorType_CloudflareGlobalApiKey, + Raw: []byte(apiKeyRes), + }) + continue + } + + for emailMatch := range uniqueEmailMatches { + s1 := detectors.Result{ + DetectorType: detector_typepb.DetectorType_CloudflareGlobalApiKey, + Redacted: emailMatch, + Raw: []byte(apiKeyRes), + RawV2: []byte(apiKeyRes + emailMatch), + } + + if verify { + s1.Verified = v1.VerifyGlobalAPIKey(ctx, client, apiKeyRes, emailMatch) + } + + results = append(results, s1) + } + } + + return results, nil +} + +func (s Scanner) Type() detector_typepb.DetectorType { + return detector_typepb.DetectorType_CloudflareGlobalApiKey +} + +func (s Scanner) Description() string { + return "Cloudflare is a web infrastructure and website security company. Its services include content delivery network (CDN), DDoS mitigation, Internet security, and distributed domain name server (DNS) services. Cloudflare API keys (cfk_ prefixed, 2026+ format) can be used to access and modify these services." +} diff --git a/pkg/detectors/cloudflareglobalapikey/v2/cloudflareglobalapikey_test.go b/pkg/detectors/cloudflareglobalapikey/v2/cloudflareglobalapikey_test.go new file mode 100644 index 000000000000..ed0f82eaef1f --- /dev/null +++ b/pkg/detectors/cloudflareglobalapikey/v2/cloudflareglobalapikey_test.go @@ -0,0 +1,81 @@ +package cloudflareglobalapikey + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" + "github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick" +) + +var ( + validV2Pattern = "cfk_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfe / testuser1005@example.com" +) + +func TestCloudFlareGlobalAPIKeyV2_Pattern(t *testing.T) { + d := Scanner{} + ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d}) + + tests := []struct { + name string + input string + want []string + }{ + { + name: "valid v2 pattern - no keyword proximity needed", + input: fmt.Sprintf("some config: %s", validV2Pattern), + want: []string{"cfk_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfetestuser1005@example.com"}, + }, + { + name: "valid v2 pattern - no email nearby still emits result", + input: "API_KEY=cfk_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfe", + want: []string{"cfk_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfe"}, + }, + { + name: "no match for legacy format", + input: "cfk_: abcdef1234567890abcdef1234567890abcdef0 / testuser1005@example.com", + want: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input)) + if len(matchedDetectors) == 0 && test.want != nil { + t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input) + return + } + + results, err := d.FromData(context.Background(), false, []byte(test.input)) + if err != nil { + t.Errorf("error = %v", err) + return + } + + 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) + } + }) + } +} diff --git a/pkg/engine/defaults/defaults.go b/pkg/engine/defaults/defaults.go index 7d39fbdd931d..28365d2c3602 100644 --- a/pkg/engine/defaults/defaults.go +++ b/pkg/engine/defaults/defaults.go @@ -161,9 +161,11 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/closecrm" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudconvert" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudelements" - "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudflareapitoken" + cloudflareapitokenv1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudflareapitoken/v1" + cloudflareapitokenv2 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudflareapitoken/v2" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudflarecakey" - "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudflareglobalapikey" + cloudflareglobalapikeyv1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudflareglobalapikey/v1" + cloudflareglobalapikeyv2 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudflareglobalapikey/v2" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudimage" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudmersive" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudplan" @@ -1032,9 +1034,11 @@ func buildDetectorList() []detectors.Detector { &closecrm.Scanner{}, &cloudconvert.Scanner{}, &cloudelements.Scanner{}, - &cloudflareapitoken.Scanner{}, + &cloudflareapitokenv1.Scanner{}, + &cloudflareapitokenv2.Scanner{}, &cloudflarecakey.Scanner{}, - &cloudflareglobalapikey.Scanner{}, + &cloudflareglobalapikeyv1.Scanner{}, + &cloudflareglobalapikeyv2.Scanner{}, &cloudimage.Scanner{}, &cloudmersive.Scanner{}, &cloudplan.Scanner{},