Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
88 changes: 88 additions & 0 deletions pkg/detectors/rancher/rancher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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

// Match variable name case-insensitively via (?i:...) scope, then require strictly
// lowercase alphanumeric token to avoid false positives from the broad character set.
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, error) {
req, err := http.NewRequestWithContext(ctx, "GET", serverURL+"/v3", nil)
if err != nil {
return false, err
}
req.Header.Set("Authorization", "Bearer "+token)

res, err := client.Do(req)
if err != nil {
return false, err
}
defer func() { _ = res.Body.Close() }()
return res.StatusCode == http.StatusOK, nil
}

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.

Verification errors silently swallowed, misclassifying results

Medium Severity

verifyToken returns only bool, swallowing all errors (network timeouts, DNS failures, non-200 status codes). Because SetVerificationError is never called, the engine at engine.go:1274 misclassifies transient verification failures as definitively "unverified" instead of "unknown." Users filtering with --results=verified,unknown will silently miss these results. The established pattern (seen in apiflash.go, abstract.go, and most other detectors) is to return (bool, error) and call s1.SetVerificationError(...).

Additional Locations (1)
Fix in Cursor Fix in Web

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]), "/")
verified, verifyErr := verifyToken(ctx, serverURL, token)
if verifyErr != nil {
s1.SetVerificationError(verifyErr, token)
} else {
s1.Verified = verified
}
}

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 @@ -609,6 +609,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/rabbitmq"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/railwayapp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/ramp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/rancher"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/rapidapi"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/rawg"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/razorpay"
Expand Down Expand Up @@ -1501,6 +1502,7 @@ func buildDetectorList() []detectors.Detector {
&rabbitmq.Scanner{},
&railwayapp.Scanner{},
&ramp.Scanner{},
&rancher.Scanner{},
&rapidapi.Scanner{},
// &raven.Scanner{},
&rawg.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;
}