-
Notifications
You must be signed in to change notification settings - Fork 2.5k
feat: add Rancher/Cattle token detector #4997
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
ffc2490
9bf0780
df672a6
19490ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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})["']?`) | ||
|
|
||
| // 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 | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verification errors silently swallowed, misclassifying resultsMedium Severity
Additional Locations (1)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." | ||
| } | ||
| 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) | ||
| } | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Import and registration placed out of alphabetical orderLow Severity The Additional Locations (1)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" | ||
|
|
@@ -1605,6 +1606,7 @@ func buildDetectorList() []detectors.Detector { | |
| &sourcegraph.Scanner{}, | ||
| &sourcegraphcody.Scanner{}, | ||
| // &sparkpost.Scanner{}, | ||
| &rancher.Scanner{}, | ||
| &spectralops.Scanner{}, | ||
| &speechtextai.Scanner{}, | ||
| &splunkobservabilitytoken.Scanner{}, | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1054,4 +1054,5 @@ enum DetectorType { | |
| GitLabOauth2 = 1050; | ||
| SpectralOps = 1051; | ||
| AWSAppSync = 1052; | ||
| RancherToken = 1053; | ||
| } | ||


There was a problem hiding this comment.
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 ofkeyPatapplies 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.Reviewed by Cursor Bugbot for commit 9bf0780. Configure here.