Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
77 changes: 77 additions & 0 deletions pkg/detectors/rancher/rancher.go
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{}
Copy link
Copy Markdown

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 Scanner struct doesn't embed detectors.DefaultMultiPartCredentialProvider, even though the detector requires two distinct patterns (server context via serverPattern and secret via tokenPattern) 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 to FromData may 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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 74f5a74. Configure here.


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})`,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Token regex won't match real Rancher token format

High Severity

Real Rancher API tokens use the format token-xxxxx:yyyyyyyyyy (containing hyphens and colons), as documented in Rancher's official API docs. The capture group [a-z0-9]{54,64} only allows lowercase alphanumerics, so it will never match actual CATTLE_TOKEN or RANCHER_TOKEN values. The test data uses a fabricated token (kubeadmin5f8a3b...) that doesn't resemble any real Rancher token format, masking this fundamental mismatch.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 86cc6fa. Configure here.

serverPattern = regexp.MustCompile(
`(?i)(?:CATTLE_SERVER|RANCHER_URL|rancher\.[a-z0-9-]+\.[a-z]{2,})`,
)
Comment thread
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)
Comment thread
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."
}
102 changes: 102 additions & 0 deletions pkg/detectors/rancher/rancher_test.go
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)
}
}
})
}
}
4 changes: 3 additions & 1 deletion pkg/engine/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/storychief"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/strava"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/streak"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/rancher"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/stripe"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/stripepaymentintent"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/stripo"
Expand Down Expand Up @@ -1612,7 +1613,8 @@ func buildDetectorList() []detectors.Detector {
&storychief.Scanner{},
&strava.Scanner{},
&streak.Scanner{},
&stripe.Scanner{},
&rancher.Scanner{},
&stripe.Scanner{},
&stripepaymentintent.Scanner{},
&stripo.Scanner{},
&stytch.Scanner{},
Expand Down
3 changes: 3 additions & 0 deletions pkg/pb/detectorspb/detectors.pb.go

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

Loading