Update Cloudflare detectors for 2026+ prefixed credential formats (include upstream PR changes) (#5111)
* 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.
* Address PR feedback on Cloudflare detectors
## Context/Background
Addressing reviewer feedback from @MuneebUllahKhan222 and Cursor
Bugbot on PR #4830.
## Changes in this commit
- Verification functions now return `(bool, error)` with proper
indeterminate/determinate handling. Callsites use
`SetVerificationError`. Status checks use exact `http.StatusOK`
instead of range. Response bodies properly drained via
`io.Copy(io.Discard, ...)`.
- Extracted shared verification functions into `common.go` at each
detector package root (following the AWS detector pattern). v1 and
v2 both import from the parent package instead of v2 importing v1.
- Added integration tests for both v2 detectors with coverage for
`cfk_`, `cfut_`, and `cfat_` (including account-scoped
verification).
- `cfat_` tokens now pair with account IDs in `RawV2` regardless of
the `verify` flag, consistent with how `cfk_` pairs with emails.
* Resolved Comments
---------
Co-authored-by: Nicholas Comer <[email protected]>
This commit is contained in:
co-authored by
Nicholas Comer
parent
3cbcd39c7e
commit
e1f9e5d1d5
@@ -560,6 +560,8 @@ func run(state overseer.State, logSync func() error) {
|
||||
feature.LobDetectorEnabled.Store(true)
|
||||
feature.HashiCorpVaultBatchTokenDetectorEnabled.Store(true)
|
||||
feature.HashiCorpVaultTokenDetectorEnabled.Store(true)
|
||||
feature.CloudflareApiTokenV2DetectorEnabled.Store(true)
|
||||
feature.CloudflareGlobalApiKeyV2DetectorEnabled.Store(true)
|
||||
|
||||
conf := &config.Config{}
|
||||
if *configFilename != "" {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package cloudflareapitoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// VerifyUserToken checks if a Cloudflare user API token is valid.
|
||||
// Returns (true, nil) if verified, (false, nil) for determinate auth
|
||||
// failures, and (false, err) for indeterminate failures.
|
||||
func VerifyUserToken(ctx context.Context, client *http.Client, token string) (bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.cloudflare.com/client/v4/user/tokens/verify", nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
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, err
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, res.Body)
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
|
||||
switch res.StatusCode {
|
||||
case http.StatusOK:
|
||||
return true, nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyAccountToken checks if a Cloudflare account API token is
|
||||
// valid for the given account ID. Returns (true, nil) if verified,
|
||||
// (false, nil) for determinate auth failures, and (false, err) for
|
||||
// indeterminate failures.
|
||||
func VerifyAccountToken(ctx context.Context, client *http.Client, token, accountID string) (bool, error) {
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/accounts/%s/tokens/verify", accountID)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
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, err
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, res.Body)
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
|
||||
switch res.StatusCode {
|
||||
case http.StatusOK:
|
||||
return true, nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
|
||||
}
|
||||
}
|
||||
+8
-16
@@ -2,21 +2,23 @@ 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"
|
||||
cfapitoken "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudflareapitoken"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
|
||||
)
|
||||
|
||||
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()
|
||||
@@ -46,19 +48,9 @@ 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 func() { _ = res.Body.Close() }()
|
||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||
s1.Verified = true
|
||||
}
|
||||
}
|
||||
isVerified, verificationErr := cfapitoken.VerifyUserToken(ctx, client, resMatch)
|
||||
s1.Verified = isVerified
|
||||
s1.SetVerificationError(verificationErr, resMatch)
|
||||
}
|
||||
|
||||
results = append(results, s1)
|
||||
@@ -0,0 +1,107 @@
|
||||
package cloudflareapitoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
cfapitoken "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudflareapitoken"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
|
||||
)
|
||||
|
||||
type Scanner struct {
|
||||
detectors.DefaultMultiPartCredentialProvider
|
||||
}
|
||||
|
||||
// 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.
|
||||
keyPat = 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 := keyPat.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 strings.HasPrefix(resMatch, "cfat_") {
|
||||
// Account tokens: pair with each nearby account ID.
|
||||
if len(uniqueAccountIDs) == 0 {
|
||||
// No account ID found; still report the token.
|
||||
results = append(results, detectors.Result{
|
||||
DetectorType: detector_typepb.DetectorType_CloudflareApiToken,
|
||||
Raw: []byte(resMatch),
|
||||
SecretParts: map[string]string{"key": resMatch},
|
||||
})
|
||||
continue
|
||||
}
|
||||
for accountID := range uniqueAccountIDs {
|
||||
s1 := detectors.Result{
|
||||
DetectorType: detector_typepb.DetectorType_CloudflareApiToken,
|
||||
Raw: []byte(resMatch),
|
||||
RawV2: []byte(resMatch + accountID),
|
||||
SecretParts: map[string]string{
|
||||
"key": resMatch,
|
||||
"account_id": accountID,
|
||||
},
|
||||
}
|
||||
if verify {
|
||||
isVerified, verificationErr := cfapitoken.VerifyAccountToken(ctx, client, resMatch, accountID)
|
||||
s1.Verified = isVerified
|
||||
s1.SetVerificationError(verificationErr, resMatch)
|
||||
}
|
||||
results = append(results, s1)
|
||||
}
|
||||
} else {
|
||||
// cfut_ tokens use the user token verification endpoint.
|
||||
s1 := detectors.Result{
|
||||
DetectorType: detector_typepb.DetectorType_CloudflareApiToken,
|
||||
Raw: []byte(resMatch),
|
||||
SecretParts: map[string]string{"key": resMatch},
|
||||
}
|
||||
if verify {
|
||||
isVerified, verificationErr := cfapitoken.VerifyUserToken(ctx, client, resMatch)
|
||||
s1.Verified = isVerified
|
||||
s1.SetVerificationError(verificationErr, resMatch)
|
||||
}
|
||||
results = append(results, s1)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
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."
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//go:build detectors
|
||||
// +build detectors
|
||||
|
||||
package cloudflareapitoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
|
||||
)
|
||||
|
||||
func TestCloudflareApiTokenV2_FromChunk(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
|
||||
if err != nil {
|
||||
t.Fatalf("could not get test secrets from GCP: %s", err)
|
||||
}
|
||||
|
||||
userToken := testSecrets.MustGetField("CLOUDFLARE_USER_API_TOKEN_V2")
|
||||
userTokenInactive := testSecrets.MustGetField("CLOUDFLARE_USER_API_TOKEN_V2_INACTIVE")
|
||||
accountToken := testSecrets.MustGetField("CLOUDFLARE_ACCOUNT_API_TOKEN_V2")
|
||||
accountID := testSecrets.MustGetField("CLOUDFLARE_ACCOUNT_API_TOKEN_V2_ACCOUNT_ID")
|
||||
accountTokenInactive := testSecrets.MustGetField("CLOUDFLARE_ACCOUNT_API_TOKEN_V2_INACTIVE")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
data []byte
|
||||
verify bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
s Scanner
|
||||
args args
|
||||
want []detectors.Result
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "cfut_ found, verified",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("cloudflare token %s", userToken)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detector_typepb.DetectorType_CloudflareApiToken,
|
||||
Verified: true,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "cfut_ found, unverified",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("cloudflare token %s", userTokenInactive)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detector_typepb.DetectorType_CloudflareApiToken,
|
||||
Verified: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "cfat_ found, verified",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("cloudflare token %s account %s", accountToken, accountID)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detector_typepb.DetectorType_CloudflareApiToken,
|
||||
Verified: true,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "cfat_ found, unverified",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("cloudflare token %s account %s", accountTokenInactive, accountID)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detector_typepb.DetectorType_CloudflareApiToken,
|
||||
Verified: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte("You cannot find the secret within"),
|
||||
verify: true,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := Scanner{}
|
||||
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CloudflareApiTokenV2.FromData() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
if len(got[i].Raw) == 0 {
|
||||
t.Fatalf("no raw secret present: \n %+v", got[i])
|
||||
}
|
||||
got[i].Raw = nil
|
||||
got[i].RawV2 = nil
|
||||
}
|
||||
if diff := pretty.Compare(got, tt.want); diff != "" {
|
||||
t.Errorf("CloudflareApiTokenV2.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromData(benchmark *testing.B) {
|
||||
ctx := context.Background()
|
||||
s := Scanner{}
|
||||
for name, data := range detectors.MustGetBenchmarkData() {
|
||||
benchmark.Run(name, func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, err := s.FromData(ctx, false, data)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
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 - go http client",
|
||||
input: `
|
||||
func setupCloudflareClient() (*http.Client, error) {
|
||||
req, err := http.NewRequest("GET", "https://api.cloudflare.com/client/v4/user", http.NoBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Rotated to the new self-identifying user token format during the March migration.
|
||||
req.Header.Set("Authorization", "Bearer cfut_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfe")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, _ := client.Do(req)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
return client, nil
|
||||
}
|
||||
`,
|
||||
want: []string{
|
||||
"cfut_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfe",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid v2 account token - wrangler config",
|
||||
input: `
|
||||
# wrangler.toml
|
||||
name = "edge-worker"
|
||||
main = "src/index.js"
|
||||
compatibility_date = "2026-01-15"
|
||||
|
||||
[env.production]
|
||||
account_id = "a4c123b1612dd272d1371c17149d4395"
|
||||
|
||||
# CF_API_TOKEN used by the deploy pipeline
|
||||
CF_API_TOKEN = "cfat_OhbVrpoiVgRV5IfLBcbfnoGMbJmTPSIAoCLrZ3aW5da846a3"
|
||||
`,
|
||||
want: []string{
|
||||
"cfat_OhbVrpoiVgRV5IfLBcbfnoGMbJmTPSIAoCLrZ3aW5da846a3a4c123b1612dd272d1371c17149d4395",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no match for legacy format",
|
||||
input: `
|
||||
# .env.legacy
|
||||
# Pre-2026 tokens don't carry the cfut_/cfat_ prefix and shouldn't match the new pattern.
|
||||
CLOUDFLARE_API_TOKEN=kOjD1yceduu2jxL2uuwT9dkOIudU3_54sLCEud6j
|
||||
`,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "valid pattern - key rotation script with multiple tokens",
|
||||
input: `
|
||||
#!/usr/bin/env bash
|
||||
# rotate-cf-tokens.sh - retires the old worker token and provisions a replacement
|
||||
set -euo pipefail
|
||||
|
||||
echo "Retiring old token..."
|
||||
OLD_TOKEN="cfut_fygw2wMqZcUDIh7yfJs1ON43xKmTecQoXsf2o3gy8eb5bb68"
|
||||
|
||||
echo "Provisioning new token..."
|
||||
NEW_TOKEN="cfut_S7RPeMOkIUpkDyr7OSJoRu1XXdo0cZuzren68K4Ta6fce484"
|
||||
|
||||
curl -s -X DELETE "https://api.cloudflare.com/client/v4/user/tokens/verify" \
|
||||
-H "Authorization: Bearer ${OLD_TOKEN}"
|
||||
curl -s -X PUT "https://api.cloudflare.com/client/v4/user/tokens/verify" \
|
||||
-H "Authorization: Bearer ${NEW_TOKEN}"
|
||||
`,
|
||||
want: []string{
|
||||
"cfut_fygw2wMqZcUDIh7yfJs1ON43xKmTecQoXsf2o3gy8eb5bb68",
|
||||
"cfut_S7RPeMOkIUpkDyr7OSJoRu1XXdo0cZuzren68K4Ta6fce484",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - too short",
|
||||
input: `
|
||||
func setupCloudflareClient() {
|
||||
// Truncated token accidentally committed during a config export
|
||||
token := "cfut_ZE4CrcFhEIDXk9vL2sTLe"
|
||||
client.SetToken(token)
|
||||
}
|
||||
`,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - too long",
|
||||
input: `
|
||||
func setupCloudflareClient() {
|
||||
// Extra characters appended by a bad find-and-replace
|
||||
token := "cfut_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfeEXTRA"
|
||||
client.SetToken(token)
|
||||
}
|
||||
`,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - invalid characters",
|
||||
input: `
|
||||
func setupCloudflareClient() {
|
||||
// Token was mangled when pasted from a Slack message with markdown formatting
|
||||
token := "cfut_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbf!"
|
||||
client.SetToken(token)
|
||||
}
|
||||
`,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - keyword only",
|
||||
input: `
|
||||
// TODO: load the real token instead of reading cfut_ from env
|
||||
func isCloudflareUserToken(s string) bool {
|
||||
return strings.HasPrefix(s, "cfut_")
|
||||
}
|
||||
`,
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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`)
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package cloudflareglobalapikey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// VerifyGlobalAPIKey checks if a Cloudflare Global API Key is valid.
|
||||
// Returns (true, nil) if verified, (false, nil) for determinate auth
|
||||
// failures, and (false, err) for indeterminate failures.
|
||||
func VerifyGlobalAPIKey(ctx context.Context, client *http.Client, apiKey, email string) (bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.cloudflare.com/client/v4/user", nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
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, err
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, res.Body)
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
|
||||
switch res.StatusCode {
|
||||
case http.StatusOK:
|
||||
return true, nil
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
|
||||
}
|
||||
}
|
||||
+10
-18
@@ -2,13 +2,13 @@ package cloudflareglobalapikey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
cfglobalapikey "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudflareglobalapikey"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
@@ -60,21 +64,9 @@ 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 func() { _ = res.Body.Close() }()
|
||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||
s1.Verified = true
|
||||
}
|
||||
}
|
||||
isVerified, verificationErr := cfglobalapikey.VerifyGlobalAPIKey(ctx, client, apiKeyRes, emailMatch)
|
||||
s1.Verified = isVerified
|
||||
s1.SetVerificationError(verificationErr, apiKeyRes)
|
||||
}
|
||||
|
||||
results = append(results, s1)
|
||||
+3
-3
@@ -12,8 +12,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
validPattern = "abcD123efg456HIJklmn789OPQ_rstUVWxYZ-012 / [email protected]"
|
||||
invalidPattern = "abcD123efg456HIJklmn789OPQ_rstUVWxYZ-012/testing@go"
|
||||
validPattern = "abcdef1234567890abcdef1234567890abcdef0 / [email protected]"
|
||||
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-[email protected]"},
|
||||
want: []string{"abcdef1234567890abcdef1234567890abcdef0[email protected]"},
|
||||
},
|
||||
{
|
||||
name: "valid pattern - key out of prefix range",
|
||||
@@ -0,0 +1,94 @@
|
||||
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"
|
||||
cfglobalapikey "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudflareglobalapikey"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
|
||||
)
|
||||
|
||||
type Scanner struct {
|
||||
detectors.DefaultMultiPartCredentialProvider
|
||||
}
|
||||
|
||||
// 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.
|
||||
apiKeyPat = 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 := apiKeyPat.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),
|
||||
SecretParts: map[string]string{"key": apiKeyRes},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
for emailMatch := range uniqueEmailMatches {
|
||||
s1 := detectors.Result{
|
||||
DetectorType: detector_typepb.DetectorType_CloudflareGlobalApiKey,
|
||||
Redacted: emailMatch,
|
||||
Raw: []byte(apiKeyRes),
|
||||
RawV2: []byte(apiKeyRes + emailMatch),
|
||||
SecretParts: map[string]string{
|
||||
"key": apiKeyRes,
|
||||
"email": emailMatch,
|
||||
},
|
||||
}
|
||||
|
||||
if verify {
|
||||
isVerified, verificationErr := cfglobalapikey.VerifyGlobalAPIKey(ctx, client, apiKeyRes, emailMatch)
|
||||
s1.Verified = isVerified
|
||||
s1.SetVerificationError(verificationErr, apiKeyRes)
|
||||
}
|
||||
|
||||
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. Cloudflare API keys (cfk_ prefixed, 2026+ format) can be used to access and modify these services."
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
//go:build detectors
|
||||
// +build detectors
|
||||
|
||||
package cloudflareglobalapikey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
|
||||
)
|
||||
|
||||
func TestCloudflareGlobalApiKeyV2_FromChunk(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
|
||||
if err != nil {
|
||||
t.Fatalf("could not get test secrets from GCP: %s", err)
|
||||
}
|
||||
|
||||
globalApiKey := testSecrets.MustGetField("CLOUDFLARE_GLOBAL_API_KEY_V2")
|
||||
globalApiKeyEmail := testSecrets.MustGetField("CLOUDFLARE_GLOBAL_API_KEY_V2_EMAIL")
|
||||
inactiveGlobalApiKey := testSecrets.MustGetField("CLOUDFLARE_GLOBAL_API_KEY_V2_INACTIVE")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
data []byte
|
||||
verify bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
s Scanner
|
||||
args args
|
||||
want []detectors.Result
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "found, verified",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("cloudflare key %s with email %s", globalApiKey, globalApiKeyEmail)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detector_typepb.DetectorType_CloudflareGlobalApiKey,
|
||||
Redacted: globalApiKeyEmail,
|
||||
Verified: true,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, unverified",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("cloudflare key %s with email %s", inactiveGlobalApiKey, globalApiKeyEmail)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detector_typepb.DetectorType_CloudflareGlobalApiKey,
|
||||
Redacted: globalApiKeyEmail,
|
||||
Verified: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte("You cannot find the secret within"),
|
||||
verify: true,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := Scanner{}
|
||||
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CloudflareGlobalApiKeyV2.FromData() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
if len(got[i].Raw) == 0 {
|
||||
t.Fatalf("no raw secret present: \n %+v", got[i])
|
||||
}
|
||||
got[i].Raw = nil
|
||||
got[i].RawV2 = nil
|
||||
}
|
||||
if diff := pretty.Compare(got, tt.want); diff != "" {
|
||||
t.Errorf("CloudflareGlobalApiKeyV2.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromData(benchmark *testing.B) {
|
||||
ctx := context.Background()
|
||||
s := Scanner{}
|
||||
for name, data := range detectors.MustGetBenchmarkData() {
|
||||
benchmark.Run(name, func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, err := s.FromData(ctx, false, data)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package cloudflareglobalapikey
|
||||
|
||||
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 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 - curl script with account email",
|
||||
input: `
|
||||
#!/usr/bin/env bash
|
||||
# purge-cache.sh - flushes the edge cache for the production zone
|
||||
set -euo pipefail
|
||||
|
||||
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
|
||||
-H "X-Auth-Email: [email protected]" \
|
||||
-H "X-Auth-Key: cfk_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfe" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"purge_everything":true}'
|
||||
`,
|
||||
want: []string{
|
||||
"cfk_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfetestuser1005@example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid v2 pattern - env file, no email nearby still emits result",
|
||||
input: `
|
||||
# .env.production
|
||||
APP_NAME=edge-cache-worker
|
||||
CLOUDFLARE_API_KEY=cfk_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfe
|
||||
LOG_LEVEL=info
|
||||
`,
|
||||
want: []string{
|
||||
"cfk_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfe",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no match for legacy format",
|
||||
input: `
|
||||
# .env.legacy
|
||||
# Pre-2026 global API keys are bare 37-char hex strings without a prefix.
|
||||
CLOUDFLARE_API_KEY=abcdef1234567890abcdef1234567890abcdef0
|
||||
[email protected]
|
||||
`,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "valid pattern - ansible playbook rotating keys across environments",
|
||||
input: `
|
||||
---
|
||||
- name: Rotate Cloudflare global API keys
|
||||
hosts: localhost
|
||||
vars:
|
||||
staging_key: "cfk_fygw2wMqZcUDIh7yfJs1ON43xKmTecQoXsf2o3gy8eb5bb68"
|
||||
production_key: "cfk_S7RPeMOkIUpkDyr7OSJoRu1XXdo0cZuzren68K4Ta6fce484"
|
||||
tasks:
|
||||
- name: Update staging secret store
|
||||
command: "vault kv put secret/staging cf_key={{ staging_key }}"
|
||||
- name: Update production secret store
|
||||
command: "vault kv put secret/production cf_key={{ production_key }}"
|
||||
`,
|
||||
want: []string{
|
||||
"cfk_fygw2wMqZcUDIh7yfJs1ON43xKmTecQoXsf2o3gy8eb5bb68",
|
||||
"cfk_S7RPeMOkIUpkDyr7OSJoRu1XXdo0cZuzren68K4Ta6fce484",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - too short",
|
||||
input: `
|
||||
func setupCloudflareClient() {
|
||||
// Truncated key accidentally committed during a config export
|
||||
key := "cfk_ZE4CrcFhEIDXk9vL2sTLe"
|
||||
client.SetGlobalKey(key)
|
||||
}
|
||||
`,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - too long",
|
||||
input: `
|
||||
func setupCloudflareClient() {
|
||||
// Extra characters appended by a bad find-and-replace
|
||||
key := "cfk_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbfeEXTRA"
|
||||
client.SetGlobalKey(key)
|
||||
}
|
||||
`,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - invalid characters",
|
||||
input: `
|
||||
func setupCloudflareClient() {
|
||||
// Key was mangled when pasted from a Slack message with markdown formatting
|
||||
key := "cfk_ZE4CrcFhEIDXk9vL2sTLeARsFp2ZZYbydVDhhIUq8573bbf!"
|
||||
client.SetGlobalKey(key)
|
||||
}
|
||||
`,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - keyword only",
|
||||
input: `
|
||||
// TODO: load the real key instead of reading cfk_ from env
|
||||
func isCloudflareGlobalKey(s string) bool {
|
||||
return strings.HasPrefix(s, "cfk_")
|
||||
}
|
||||
`,
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -168,9 +168,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/cloudinary"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cloudmersive"
|
||||
@@ -1062,9 +1064,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{},
|
||||
&cloudinary.Scanner{},
|
||||
&cloudmersive.Scanner{},
|
||||
@@ -1848,6 +1852,10 @@ func buildDetectorList() []detectors.Detector {
|
||||
return !feature.HashiCorpVaultBatchTokenDetectorEnabled.Load()
|
||||
case *hashicorpvaulttoken.Scanner:
|
||||
return !feature.HashiCorpVaultTokenDetectorEnabled.Load()
|
||||
case *cloudflareapitokenv2.Scanner:
|
||||
return !feature.CloudflareApiTokenV2DetectorEnabled.Load()
|
||||
case *cloudflareglobalapikeyv2.Scanner:
|
||||
return !feature.CloudflareGlobalApiKeyV2DetectorEnabled.Load()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ var (
|
||||
LobDetectorEnabled atomic.Bool
|
||||
HashiCorpVaultBatchTokenDetectorEnabled atomic.Bool
|
||||
HashiCorpVaultTokenDetectorEnabled atomic.Bool
|
||||
CloudflareApiTokenV2DetectorEnabled atomic.Bool
|
||||
CloudflareGlobalApiKeyV2DetectorEnabled atomic.Bool
|
||||
)
|
||||
|
||||
type AtomicString struct {
|
||||
|
||||
Reference in New Issue
Block a user