-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat: add Rancher/Cattle token detector #4874
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
74f5a74
86414aa
7401a70
f031603
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,77 @@ | ||
| package rancher | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
|
|
||
| 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/detectorspb" | ||
| ) | ||
|
|
||
| type Scanner struct{} | ||
|
|
||
| var _ detectors.Detector = (*Scanner)(nil) | ||
|
|
||
| var ( | ||
| tokenPattern = regexp.MustCompile( | ||
| `(?i)(?:CATTLE_TOKEN|RANCHER_TOKEN|CATTLE_BOOTSTRAP_PASSWORD|RANCHER_API_TOKEN)[^\w]{1,4}([a-z0-9]{54,64})`, | ||
| ) | ||
|
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. Token regex won't match real Rancher token formatHigh Severity Real Rancher API tokens use the format Reviewed by Cursor Bugbot for commit 86cc6fa. Configure here. |
||
| serverPattern = regexp.MustCompile( | ||
| `(?i)(?:CATTLE_SERVER|RANCHER_URL|rancher\.[a-z0-9-]+\.[a-z]{2,})`, | ||
| ) | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| ) | ||
|
|
||
| func (s Scanner) Keywords() []string { | ||
| return []string{"cattle_token", "rancher_token", "rancher_api_token", "cattle_bootstrap_password"} | ||
| } | ||
|
|
||
| func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { | ||
| dataStr := string(data) | ||
|
|
||
| if !serverPattern.MatchString(dataStr) { | ||
| return | ||
| } | ||
|
|
||
| matches := tokenPattern.FindAllStringSubmatch(dataStr, -1) | ||
| for _, match := range matches { | ||
| if len(match) < 2 { | ||
| continue | ||
| } | ||
| token := match[1] | ||
|
|
||
| result := detectors.Result{ | ||
| DetectorType: detectorspb.DetectorType_Rancher, | ||
| Raw: []byte(token), | ||
| } | ||
|
|
||
| if verify { | ||
| client := common.SaneHttpClient() | ||
| req, err := http.NewRequestWithContext(ctx, "GET", "https://rancher.example.com/v3", nil) | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
| if err != nil { | ||
| continue | ||
| } | ||
| req.Header.Set("Authorization", "Bearer "+token) | ||
| res, err := client.Do(req) | ||
| if err == nil { | ||
| res.Body.Close() | ||
| if res.StatusCode == http.StatusOK { | ||
| result.Verified = true | ||
| } | ||
| } | ||
| } | ||
|
|
||
| results = append(results, result) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| func (s Scanner) Type() detectorspb.DetectorType { | ||
| return detectorspb.DetectorType_Rancher | ||
| } | ||
|
|
||
| func (s Scanner) Description() string { | ||
| return "Rancher is a Kubernetes management platform. Rancher API tokens can be used to gain full cluster admin access." | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| package rancher | ||
|
|
||
| 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 ( | ||
| validPattern = "RANCHER_URL=https://rancher.example.com\nRANCHER_API_TOKEN=kubeadmin5f8a3b2c1d9e4f7a6b0c5d2e8f1a4b7c3d6e9f2a5b8c1d4e7f0a3b6" | ||
| invalidPattern = "RANCHER_API_TOKEN=shorttoken123" | ||
| keyword = "rancher_api_token" | ||
| ) | ||
|
|
||
| func TestRancher_Pattern(t *testing.T) { | ||
| d := Scanner{} | ||
| ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d}) | ||
| tests := []struct { | ||
| name string | ||
| input string | ||
| want []string | ||
| }{ | ||
| { | ||
| name: "valid pattern with server context", | ||
| input: fmt.Sprintf("%s", validPattern), | ||
| want: []string{"kubeadmin5f8a3b2c1d9e4f7a6b0c5d2e8f1a4b7c3d6e9f2a5b8c1d4e7f0a3b6"}, | ||
| }, | ||
| { | ||
| name: "invalid pattern - token too short", | ||
| input: fmt.Sprintf("%s token = '%s'", keyword, invalidPattern), | ||
| want: []string{}, | ||
| }, | ||
| { | ||
| name: "no server context - should not detect", | ||
| input: "RANCHER_API_TOKEN=kubeadmin5f8a3b2c1d9e4f7a6b0c5d2e8f1a4b7c3d6e9f2a5b8c1d4e7f0a3b6", | ||
| want: []string{}, | ||
| }, | ||
| } | ||
|
|
||
| for _, test := range tests { | ||
| t.Run(test.name, func(t *testing.T) { | ||
| matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input)) | ||
| 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 | ||
| } | ||
|
|
||
| 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) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| 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) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.


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.
Missing multi-part credential provider causes missed detections
Medium Severity
The
Scannerstruct doesn't embeddetectors.DefaultMultiPartCredentialProvider, even though the detector requires two distinct patterns (server context viaserverPatternand secret viatokenPattern) to co-occur in the same data chunk. Without this, the Aho-Corasick span calculator uses its default 512-byte radius, so if the server URL and token are farther apart in the scanned data, the chunk delivered toFromDatamay lack one of the two patterns, causing valid credentials to be silently missed. All comparable multi-part detectors (e.g.,mattermostpersonaltoken,formsite) embed this provider.Reviewed by Cursor Bugbot for commit 74f5a74. Configure here.