-
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 1 commit
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,81 @@ | ||
| package rancher | ||
|
|
||
| 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" | ||
| "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb" | ||
| ) | ||
|
|
||
| type Scanner struct { | ||
| detectors.DefaultMultiPartCredentialProvider | ||
| } | ||
|
|
||
| var _ detectors.Detector = (*Scanner)(nil) | ||
|
|
||
| var ( | ||
| client = common.SaneHttpClient() | ||
|
|
||
| // 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})["']?`) | ||
|
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. Case-insensitive flag broadens token matching unintentionallyMedium Severity The 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 (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.TrimSpace(serverMatches[0][1]) | ||
| serverURL = strings.TrimRight(serverURL, "/") | ||
|
|
||
| req, err := http.NewRequestWithContext(ctx, "GET", serverURL+"/v3", nil) | ||
| if err != nil { | ||
| continue | ||
|
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.
|
||
| } | ||
| req.Header.Set("Authorization", "Bearer "+token) | ||
|
|
||
| res, err := client.Do(req) | ||
| if err == nil { | ||
| defer func() { _ = res.Body.Close() }() | ||
|
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.
|
||
| if res.StatusCode == http.StatusOK { | ||
| s1.Verified = true | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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.
SSRF risk: unconstrained URL with permissive HTTP client
Medium Severity
The detector uses
common.SaneHttpClient()which lacks local/private IP blocking, but theserverPatregex accepts anyhttps?://URL from scanned data. Other detectors with unconstrained user-supplied URLs (likeartifactoryandjiradatacenterpat) specifically usedetectors.DetectorHttpClientWithNoLocalAddressesto prevent SSRF. An attacker could embedCATTLE_SERVER=http://169.254.169.254in scanned content to trigger requests to cloud metadata endpoints during verification.Additional Locations (1)
pkg/detectors/rancher/rancher.go#L53-L54Reviewed by Cursor Bugbot for commit ffc2490. Configure here.