Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions pkg/detectors/rancher/rancher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package rancher

import (
"context"
"net/http"
"strings"

regexp "github.com/wasilibs/go-re2"

"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
)

type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}

var _ detectors.Detector = (*Scanner)(nil)

var (
// Use the SSRF-safe client that blocks requests to local/private IP ranges.
client = detectors.DetectorHttpClientWithNoLocalAddresses

// Rancher API tokens: 54–64 lowercase alphanumeric chars, named with cattle/rancher prefixes.
keyPat = regexp.MustCompile(`(?i)(?:CATTLE_TOKEN|RANCHER_TOKEN|CATTLE_BOOTSTRAP_PASSWORD|RANCHER_API_TOKEN)[\w]*\s*[=:]\s*["']?([a-z0-9]{54,64})["']?`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Case-insensitive flag broadens token matching unintentionally

Medium Severity

The (?i) flag at the start of keyPat applies to the entire regex, including the token capture group [a-z0-9]{54,64}. In RE2, (?i) causes [a-z] to also match uppercase letters, so the capture group effectively becomes [a-zA-Z0-9]{54,64}. This contradicts the PR's stated intent of matching only "lowercase alphanumeric characters" and weakens the false-positive suppression the narrow character class was meant to provide. The (?i) flag is needed for case-insensitive variable name matching, but it needs to be scoped (e.g., via (?i:...)) so it doesn't affect the token capture group.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9bf0780. Configure here.


// Server URL used for validation; must appear nearby in the same chunk.
serverPat = regexp.MustCompile(`(?i)(?:CATTLE_SERVER|RANCHER_URL|RANCHER_SERVER)\s*[=:]\s*["']?(https?://[^\s"']+)["']?`)
)

func (s Scanner) Keywords() []string {
return []string{"CATTLE_TOKEN", "RANCHER_TOKEN", "CATTLE_BOOTSTRAP_PASSWORD", "RANCHER_API_TOKEN"}
}

func verifyToken(ctx context.Context, serverURL, token string) bool {
req, err := http.NewRequestWithContext(ctx, "GET", serverURL+"/v3", nil)
if err != nil {
return false
}
req.Header.Set("Authorization", "Bearer "+token)

res, err := client.Do(req)
if err != nil {
return false
}
defer func() { _ = res.Body.Close() }()
return res.StatusCode == http.StatusOK
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verification errors silently swallowed, misclassifying results

Medium Severity

verifyToken returns only bool, swallowing all errors (network timeouts, DNS failures, non-200 status codes). Because SetVerificationError is never called, the engine at engine.go:1274 misclassifies transient verification failures as definitively "unverified" instead of "unknown." Users filtering with --results=verified,unknown will silently miss these results. The established pattern (seen in apiflash.go, abstract.go, and most other detectors) is to return (bool, error) and call s1.SetVerificationError(...).

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit df672a6. Configure here.


func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data)

tokenMatches := keyPat.FindAllStringSubmatch(dataStr, -1)
serverMatches := serverPat.FindAllStringSubmatch(dataStr, -1)

for _, tokenMatch := range tokenMatches {
token := strings.TrimSpace(tokenMatch[1])

s1 := detectors.Result{
DetectorType: detector_typepb.DetectorType_RancherToken,
Raw: []byte(token),
SecretParts: map[string]string{"token": token},
}

if verify && len(serverMatches) > 0 {
serverURL := strings.TrimRight(strings.TrimSpace(serverMatches[0][1]), "/")
s1.Verified = verifyToken(ctx, serverURL, token)
}

results = append(results, s1)
}

return results, nil
}

func (s Scanner) Type() detector_typepb.DetectorType {
return detector_typepb.DetectorType_RancherToken
}

func (s Scanner) Description() string {
return "Rancher is a Kubernetes management platform. Rancher API tokens provide full cluster admin access and must be protected."
}
78 changes: 78 additions & 0 deletions pkg/detectors/rancher/rancher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package rancher

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"

"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)

var (
// Fake token and server for pattern matching tests only.
validInput = `
CATTLE_SERVER=https://rancher.example.com
CATTLE_TOKEN=jswpl27hs8pd88rmw2mgfgrjtpljp85fd5v7rhdwr2s6z22hvt6vjt
`
validToken = "jswpl27hs8pd88rmw2mgfgrjtpljp85fd5v7rhdwr2s6z22hvt6vjt"

invalidInput = `
# random string without Rancher context
random_data = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuv"
`
)

func TestRancher_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})

tests := []struct {
name string
input string
want []string
}{
{
name: "valid CATTLE_TOKEN pattern",
input: validInput,
want: []string{validToken},
},
{
name: "no match without cattle/rancher variable name",
input: invalidInput,
want: []string{},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(test.want) == 0 {
if len(matchedDetectors) != 0 {
t.Errorf("expected no matches, got %d", len(matchedDetectors))
}
return
}
if len(matchedDetectors) == 0 {
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
}

got := make([]string, len(results))
for i, r := range results {
got[i] = string(r.Raw)
}

if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}
}
2 changes: 2 additions & 0 deletions pkg/engine/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/sonarcloud"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/sourcegraph"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/sourcegraphcody"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/rancher"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import and registration placed out of alphabetical order

Low Severity

The rancher import and &rancher.Scanner{} registration are placed between sourcegraphcody and spectralops (in the 's' section) instead of in the 'r' section between ramp and rapidapi where they belong alphabetically. All other detectors in this file follow strict alphabetical ordering.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9bf0780. Configure here.

"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/spectralops"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/speechtextai"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/splunkobservabilitytoken"
Expand Down Expand Up @@ -1605,6 +1606,7 @@ func buildDetectorList() []detectors.Detector {
&sourcegraph.Scanner{},
&sourcegraphcody.Scanner{},
// &sparkpost.Scanner{},
&rancher.Scanner{},
&spectralops.Scanner{},
&speechtextai.Scanner{},
&splunkobservabilitytoken.Scanner{},
Expand Down
3 changes: 3 additions & 0 deletions pkg/pb/detector_typepb/detector_type.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions proto/detector_type.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1054,4 +1054,5 @@ enum DetectorType {
GitLabOauth2 = 1050;
SpectralOps = 1051;
AWSAppSync = 1052;
RancherToken = 1053;
}