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
81 changes: 81 additions & 0 deletions pkg/detectors/rancher/rancher.go
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()

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.

SSRF risk: unconstrained URL with permissive HTTP client

Medium Severity

The detector uses common.SaneHttpClient() which lacks local/private IP blocking, but the serverPat regex accepts any https?:// URL from scanned data. Other detectors with unconstrained user-supplied URLs (like artifactory and jiradatacenterpat) specifically use detectors.DetectorHttpClientWithNoLocalAddresses to prevent SSRF. An attacker could embed CATTLE_SERVER=http://169.254.169.254 in scanned content to trigger requests to cloud metadata endpoints during verification.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ffc2490. Configure here.


// 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 (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

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.

continue silently drops detected secrets during verification failure

High Severity

When http.NewRequestWithContext returns an error (e.g., due to a malformed server URL), the continue statement skips the results = append(results, s1) on line 69. This causes a legitimately detected secret to be silently dropped instead of being reported as an unverified finding. Every other multi-part detector in this codebase (e.g., databrickstoken, grafana) always appends the result regardless of verification outcome.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ffc2490. Configure here.

}
req.Header.Set("Authorization", "Bearer "+token)

res, err := client.Do(req)
if err == nil {
defer func() { _ = res.Body.Close() }()

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.

defer inside loop delays response body cleanup

Medium Severity

The defer res.Body.Close() is inside the for loop, so response bodies accumulate and are only closed when FromData returns rather than after each iteration. Every comparable detector in this codebase extracts verification into a separate function where defer executes promptly upon that function's return. With multiple token matches, this leaks open connections during the loop.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ffc2490. Configure here.

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."
}
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;
}