[Detector]-Detector for tableau personal access token (#4261)
* add detector for tableau personal access token * add test for tableau detector * removed unnecessary checks * add cloud endpoint for tableau * cleanup: simplify map copying with maps.Copy * resolve comments * added correct detector type for tableau * updated tableau PAT key and corrected integration tests * resolved comments * updated test cases * resolved comments * fixed integration tests * removed redundant validation * resolved false positive issue * updated regex for pat-name * resolved comments * update regex for better token name extraction * simplify prefix regex for tableau pat name * merged main into origin/detector/tableau-personal-access-token --------- Co-authored-by: Amaan Ullah <[email protected]>
This commit is contained in:
co-authored by
Amaan Ullah
parent
98c36a4b2a
commit
05e2328da2
@@ -0,0 +1,263 @@
|
||||
package tableau
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"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/detectorspb"
|
||||
)
|
||||
|
||||
type Scanner struct {
|
||||
detectors.DefaultMultiPartCredentialProvider
|
||||
detectors.EndpointSetter
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// Ensure the Scanner satisfies the interface at compile time.
|
||||
var _ detectors.Detector = (*Scanner)(nil)
|
||||
var _ detectors.EndpointCustomizer = (*Scanner)(nil)
|
||||
|
||||
func (Scanner) CloudEndpoint() string { return "" }
|
||||
|
||||
var (
|
||||
defaultClient = common.SaneHttpClient()
|
||||
|
||||
// Simplified token name pattern using PrefixRegex
|
||||
tokenNamePat = regexp.MustCompile(detectors.PrefixRegex([]string{"pat", "token", "name", "tableau"}) + `([a-zA-Z][a-zA-Z0-9_-]{2,50})`)
|
||||
|
||||
// Pattern for Personal Access Token Secrets
|
||||
tokenSecretPat = regexp.MustCompile(`\b([A-Za-z0-9+/]{22}==:[A-Za-z0-9]{32})\b`)
|
||||
|
||||
// Simplified Tableau Server URLs pattern
|
||||
tableauURLPat = regexp.MustCompile(`\b([a-zA-Z0-9\-]+\.online\.tableau\.com)\b`)
|
||||
)
|
||||
|
||||
// Keywords are used for efficiently pre-filtering chunks.
|
||||
func (s Scanner) Keywords() []string {
|
||||
return []string{
|
||||
"tableau",
|
||||
"online.tableau.com",
|
||||
}
|
||||
}
|
||||
|
||||
func (s Scanner) getClient() *http.Client {
|
||||
if s.client != nil {
|
||||
return s.client
|
||||
}
|
||||
return defaultClient
|
||||
}
|
||||
|
||||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
||||
dataStr := string(data)
|
||||
|
||||
// Extract token names, secrets, and URLs separately
|
||||
tokenNames := extractTokenNames(dataStr)
|
||||
tokenSecrets := extractTokenSecrets(dataStr)
|
||||
foundURLs := extractTableauURLs(dataStr)
|
||||
|
||||
// If no names or secrets found, return empty results
|
||||
if len(tokenNames) == 0 || len(tokenSecrets) == 0 {
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Use maps to deduplicate endpoints
|
||||
var uniqueEndpoints = make(map[string]struct{})
|
||||
|
||||
// Add endpoints to the list
|
||||
for _, endpoint := range s.Endpoints(foundURLs...) {
|
||||
// Remove https:// prefix if present since we add it during verification
|
||||
endpoint = strings.TrimPrefix(endpoint, "https://")
|
||||
uniqueEndpoints[endpoint] = struct{}{}
|
||||
}
|
||||
|
||||
// Process each combination of token name, token secret, and endpoint
|
||||
for _, tokenName := range tokenNames {
|
||||
for _, tokenSecret := range tokenSecrets {
|
||||
for endpoint := range uniqueEndpoints {
|
||||
result := detectors.Result{
|
||||
DetectorType: detectorspb.DetectorType_TableauPersonalAccessToken,
|
||||
Raw: []byte(tokenName),
|
||||
RawV2: []byte(fmt.Sprintf("%s:%s:%s", tokenName, tokenSecret, endpoint)),
|
||||
ExtraData: make(map[string]string),
|
||||
}
|
||||
|
||||
if verify {
|
||||
client := s.getClient()
|
||||
isVerified, extraData, verificationErr := verifyTableauPAT(ctx, client, tokenName, tokenSecret, endpoint)
|
||||
result.Verified = isVerified
|
||||
maps.Copy(result.ExtraData, extraData)
|
||||
result.SetVerificationError(verificationErr, tokenName, tokenSecret, endpoint)
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// extractTokenNames finds all potential token names in the data
|
||||
func extractTokenNames(data string) []string {
|
||||
var names []string
|
||||
// Create a map of false positive terms
|
||||
falsePositives := map[detectors.FalsePositive]struct{}{
|
||||
detectors.FalsePositive("com"): {},
|
||||
}
|
||||
|
||||
for _, match := range tokenNamePat.FindAllStringSubmatch(data, -1) {
|
||||
if len(match) >= 2 {
|
||||
name := strings.TrimSpace(match[1])
|
||||
isFalsePositive, _ := detectors.IsKnownFalsePositive(name, falsePositives, false)
|
||||
if !isFalsePositive {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// extractTokenSecrets finds all potential token secrets in the data
|
||||
func extractTokenSecrets(data string) []string {
|
||||
var secrets []string
|
||||
for _, match := range tokenSecretPat.FindAllStringSubmatch(data, -1) {
|
||||
if len(match) >= 2 {
|
||||
secret := strings.TrimSpace(match[1])
|
||||
secrets = append(secrets, secret)
|
||||
}
|
||||
}
|
||||
return secrets
|
||||
}
|
||||
|
||||
// extractTableauURLs finds all potential Tableau server URLs in the data
|
||||
func extractTableauURLs(data string) []string {
|
||||
var urls []string
|
||||
|
||||
for _, match := range tableauURLPat.FindAllStringSubmatch(data, -1) {
|
||||
if len(match) >= 2 {
|
||||
url := strings.TrimSpace(match[1])
|
||||
if url != "" {
|
||||
urls = append(urls, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return urls
|
||||
}
|
||||
|
||||
// TableauAuthRequest represents the authentication request structure
|
||||
type TableauAuthRequest struct {
|
||||
Credentials TableauCredentials `json:"credentials"`
|
||||
}
|
||||
|
||||
type TableauCredentials struct {
|
||||
PersonalAccessTokenName string `json:"personalAccessTokenName"`
|
||||
PersonalAccessTokenSecret string `json:"personalAccessTokenSecret"`
|
||||
Site interface{} `json:"site"`
|
||||
}
|
||||
|
||||
// TableauAuthResponse represents the authentication response structure
|
||||
type TableauAuthResponse struct {
|
||||
Credentials struct {
|
||||
Site struct {
|
||||
ID string `json:"id"`
|
||||
ContentURL string `json:"contentUrl"`
|
||||
} `json:"site"`
|
||||
User struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"user"`
|
||||
Token string `json:"token"`
|
||||
} `json:"credentials"`
|
||||
}
|
||||
|
||||
// verifyTableauPAT verifies a Tableau Personal Access Token by attempting authentication
|
||||
func verifyTableauPAT(ctx context.Context, client *http.Client, tokenName, tokenSecret, endpoint string) (bool, map[string]string, error) {
|
||||
// Build the verification URL
|
||||
verifyURL := fmt.Sprintf("https://%s/api/3.26/auth/signin", endpoint)
|
||||
|
||||
// Prepare metadata early - before any potential errors
|
||||
extraData := map[string]string{
|
||||
"verification_endpoint": verifyURL,
|
||||
"verification_method": "tableau_pat_auth",
|
||||
"tableau_endpoint": endpoint,
|
||||
}
|
||||
|
||||
// Rest of your verification logic...
|
||||
authReq := TableauAuthRequest{
|
||||
Credentials: TableauCredentials{
|
||||
PersonalAccessTokenName: tokenName,
|
||||
PersonalAccessTokenSecret: tokenSecret,
|
||||
Site: map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
|
||||
// Marshal to JSON
|
||||
jsonData, err := json.Marshal(authReq)
|
||||
if err != nil {
|
||||
return false, nil, fmt.Errorf("failed to marshal auth request: %v", err)
|
||||
}
|
||||
|
||||
// Create HTTP request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, verifyURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return false, nil, fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
// Execute request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// Check if it's a DNS/network error
|
||||
if strings.Contains(err.Error(), "no such host") ||
|
||||
strings.Contains(err.Error(), "dial tcp") ||
|
||||
strings.Contains(err.Error(), "connection refused") {
|
||||
extraData["network_error"] = "true"
|
||||
return false, extraData, nil // No error, just invalid endpoint
|
||||
}
|
||||
return false, nil, fmt.Errorf("request failed: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
// Read the response
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, nil, fmt.Errorf("failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
// Status code handling...
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
var authResp TableauAuthResponse
|
||||
if err := json.Unmarshal(bodyBytes, &authResp); err != nil {
|
||||
return true, extraData, err
|
||||
}
|
||||
return true, extraData, nil
|
||||
|
||||
case http.StatusUnauthorized, http.StatusBadRequest, http.StatusForbidden:
|
||||
return false, extraData, nil
|
||||
|
||||
default:
|
||||
return false, extraData, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func (s Scanner) Type() detectorspb.DetectorType {
|
||||
return detectorspb.DetectorType_TableauPersonalAccessToken
|
||||
}
|
||||
|
||||
func (s Scanner) Description() string {
|
||||
return "Tableau is a data visualization and business intelligence platform. Personal Access Tokens (PATs) provide programmatic access to Tableau Server/Online APIs and can be used to authenticate applications and automate workflows."
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
//go:build detectors
|
||||
// +build detectors
|
||||
|
||||
package tableau
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
|
||||
)
|
||||
|
||||
func TestTableau_FromChunk(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
|
||||
if err != nil {
|
||||
t.Fatalf("could not get test secrets from GCP: %s", err)
|
||||
}
|
||||
|
||||
tokenName := testSecrets.MustGetField("TABLEAU_TOKEN_NAME")
|
||||
tokenSecret := testSecrets.MustGetField("TABLEAU_TOKEN_SECRET")
|
||||
inactiveTokenName := testSecrets.MustGetField("TABLEAU_INACTIVE_TOKEN_NAME")
|
||||
inactiveTokenSecret := testSecrets.MustGetField("TABLEAU_INACTIVE_TOKEN_SECRET")
|
||||
tableauURL := testSecrets.MustGetField("TABLEAU_VALID_POD_NAME")
|
||||
invalidURL := testSecrets.MustGetField("TABLEAU_INVALID_POD_NAME")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
data []byte
|
||||
verify bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
s Scanner
|
||||
args args
|
||||
want []detectors.Result
|
||||
wantErr bool
|
||||
wantVerificationErr bool
|
||||
}{
|
||||
{
|
||||
name: "found, verified with URL",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("token=%s\nsecret=%s\nserver=%s", tokenName, tokenSecret, tableauURL)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_TableauPersonalAccessToken,
|
||||
Verified: true,
|
||||
ExtraData: map[string]string{
|
||||
"token_name": tokenName,
|
||||
"token_secret": tokenSecret,
|
||||
"endpoint": tableauURL,
|
||||
"credential_type": "personal_access_token",
|
||||
"verification_endpoint": "https://" + tableauURL + "/api/3.26/auth/signin",
|
||||
"http_status": "200",
|
||||
"verification_method": "tableau_pat_auth",
|
||||
"verification_status": "valid",
|
||||
"auth_token_received": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, verified but unexpected api surface",
|
||||
s: Scanner{client: common.ConstantResponseHttpClient(500, "Internal Server Error")},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("name = '%s'\n secret = '%s'\nserver = '%s'", tokenName, tokenSecret, tableauURL)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_TableauPersonalAccessToken,
|
||||
Verified: false,
|
||||
ExtraData: map[string]string{
|
||||
"token_name": tokenName,
|
||||
"token_secret": tokenSecret,
|
||||
"endpoint": tableauURL,
|
||||
"credential_type": "personal_access_token",
|
||||
"verification_endpoint": "https://" + tableauURL + "/api/3.26/auth/signin",
|
||||
"http_status": "500",
|
||||
"verification_method": "tableau_pat_auth",
|
||||
"verification_status": "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
{
|
||||
name: "found, unverified with invalid URL",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("name = '%s'\nsecret = '%s'\nserver = '%s'", tokenName, tokenSecret, invalidURL)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_TableauPersonalAccessToken,
|
||||
Verified: false,
|
||||
ExtraData: map[string]string{
|
||||
"token_name": tokenName,
|
||||
"token_secret": tokenSecret,
|
||||
"endpoint": invalidURL,
|
||||
"credential_type": "personal_access_token",
|
||||
"verification_endpoint": "https://" + invalidURL + "/api/3.26/auth/signin",
|
||||
"http_status": "404", // or whatever status invalid URL returns
|
||||
"verification_method": "tableau_pat_auth",
|
||||
"verification_status": "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, unverified with inactive token",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("name = '%s'\n secret = '%s'\nserver = '%s'", inactiveTokenName, inactiveTokenSecret, tableauURL)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_TableauPersonalAccessToken,
|
||||
Verified: false,
|
||||
ExtraData: map[string]string{
|
||||
"token_name": inactiveTokenName,
|
||||
"token_secret": inactiveTokenSecret,
|
||||
"endpoint": tableauURL,
|
||||
"credential_type": "personal_access_token",
|
||||
"verification_endpoint": "https://" + tableauURL + "/api/3.26/auth/signin",
|
||||
"http_status": "401",
|
||||
"verification_method": "tableau_pat_auth",
|
||||
"verification_status": "invalid",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte("You cannot find the tableau secret within"),
|
||||
verify: true,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, unverified - malformed token",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte("tableau pat_name = 'TestToken'\ntableau pat_secret = 'malformed_secret_format'"),
|
||||
verify: true,
|
||||
},
|
||||
want: nil, // Should not find due to invalid secret format
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "found multiple, mixed verification results with URLs",
|
||||
s: Scanner{},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf(`
|
||||
name1 = '%s'
|
||||
name2 = '%s'
|
||||
secret = '%s'
|
||||
secret2 = '%s'
|
||||
server1 = '%s'
|
||||
server2 = '%s'
|
||||
`, tokenName, inactiveTokenName, tokenSecret, inactiveTokenSecret, tableauURL, invalidURL)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_TableauPersonalAccessToken,
|
||||
Verified: true, // tokenName + tokenSecret + valid URL
|
||||
},
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_TableauPersonalAccessToken,
|
||||
Verified: false, // tokenName + tokenSecret + invalid URL
|
||||
},
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_TableauPersonalAccessToken,
|
||||
Verified: false, // tokenName + inactiveTokenSecret + valid URL
|
||||
},
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_TableauPersonalAccessToken,
|
||||
Verified: false, // tokenName + inactiveTokenSecret + invalid URL
|
||||
},
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_TableauPersonalAccessToken,
|
||||
Verified: false, // inactiveTokenName + tokenSecret + valid URL
|
||||
},
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_TableauPersonalAccessToken,
|
||||
Verified: false, // inactiveTokenName + tokenSecret + invalid URL
|
||||
},
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_TableauPersonalAccessToken,
|
||||
Verified: false, // inactiveTokenName + inactiveTokenSecret + valid URL
|
||||
},
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_TableauPersonalAccessToken,
|
||||
Verified: false, // inactiveTokenName + inactiveTokenSecret + invalid URL
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Enable found endpoints for tests that need URL detection
|
||||
scanner := tt.s
|
||||
scanner.UseFoundEndpoints(true)
|
||||
|
||||
got, err := scanner.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Tableau.FromData() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Check that we got the expected number of results
|
||||
if len(got) != len(tt.want) {
|
||||
t.Errorf("Tableau.FromData() got %d results, want %d", len(got), len(tt.want))
|
||||
}
|
||||
|
||||
for i := range got {
|
||||
if len(got[i].Raw) == 0 {
|
||||
t.Fatalf("no raw secret present: \n %+v", got[i])
|
||||
}
|
||||
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
|
||||
t.Errorf("Tableau.FromData() verificationError = %v, wantVerificationErr %v", got[i].VerificationError(), tt.wantVerificationErr)
|
||||
}
|
||||
}
|
||||
|
||||
ignoreOpts := []cmp.Option{
|
||||
cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "ExtraData"),
|
||||
cmpopts.IgnoreUnexported(detectors.Result{}),
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(got, tt.want, ignoreOpts...); diff != "" {
|
||||
t.Errorf("Tableau.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package tableau
|
||||
|
||||
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 (
|
||||
validPATName = "test-token-6"
|
||||
validPATSecret = "YMqNfVWiTSa0QgpoJ9GpCw==:KTamjuKrXF5gVETjIJafBBvP8Ctj5aJ3"
|
||||
invalidPATSecret = "invalid-secret-format"
|
||||
// Tableau Online endpoints for testing
|
||||
validTableauURL = "prod-ansouthgest-a.online.tableau.com"
|
||||
valid2ndTableauURL = "prod.online.tableau.com"
|
||||
invalidTableauURL = "prod.tabeau.com"
|
||||
)
|
||||
|
||||
func TestTableau_Pattern(t *testing.T) {
|
||||
d := Scanner{}
|
||||
d.UseFoundEndpoints(true) // Enable found endpoints for tests
|
||||
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "tableau_prefix_with_token_name",
|
||||
input: fmt.Sprintf("token=%s\nsecret=%s\nserver=%s", validPATName, validPATSecret, validTableauURL),
|
||||
want: []string{fmt.Sprintf("%s:%s:%s", validPATName, validPATSecret, validTableauURL)},
|
||||
description: "Tests tableau prefix directly followed by token name",
|
||||
},
|
||||
{
|
||||
name: "pat_prefix_with_name",
|
||||
input: fmt.Sprintf("pat %s\n%s\nurl=%s", validPATName, validPATSecret, validTableauURL),
|
||||
want: []string{fmt.Sprintf("%s:%s:%s", validPATName, validPATSecret, validTableauURL)},
|
||||
description: "Tests pat prefix triggering detection",
|
||||
},
|
||||
{
|
||||
name: "name_prefix_with_token",
|
||||
input: fmt.Sprintf("name %s\n%s\nurl=%s", validPATName, validPATSecret, validTableauURL),
|
||||
want: []string{fmt.Sprintf("%s:%s:%s", validPATName, validPATSecret, validTableauURL)},
|
||||
description: "Tests name prefix triggering detection",
|
||||
},
|
||||
{
|
||||
name: "single_token_multiple_urls",
|
||||
input: fmt.Sprintf("token %s\n%s\nserver1=%s\nserver2=%s", validPATName, validPATSecret, validTableauURL, valid2ndTableauURL),
|
||||
want: []string{
|
||||
fmt.Sprintf("%s:%s:%s", validPATName, validPATSecret, validTableauURL),
|
||||
fmt.Sprintf("%s:%s:%s", validPATName, validPATSecret, valid2ndTableauURL),
|
||||
},
|
||||
description: "Tests single token with multiple URLs",
|
||||
},
|
||||
{
|
||||
name: "invalid_secret_format",
|
||||
input: fmt.Sprintf("tableau %s\n%s\nserver=%s", validPATName, invalidPATSecret, validTableauURL),
|
||||
want: []string{},
|
||||
description: "Tests that invalid secret format is not detected",
|
||||
},
|
||||
{
|
||||
name: "invalid_tableau_url",
|
||||
input: fmt.Sprintf("tableau %s\n%s\nserver=%s", validPATName, validPATSecret, invalidTableauURL),
|
||||
want: []string{},
|
||||
description: "Tests that invalid Tableau URL is not detected when useFoundEndpoints is true",
|
||||
},
|
||||
{
|
||||
name: "missing_token_name",
|
||||
input: fmt.Sprintf("\n%s\nserver=%s", validPATSecret, validTableauURL),
|
||||
want: []string{},
|
||||
description: "Tests that missing token name produces no results",
|
||||
},
|
||||
{
|
||||
name: "missing_secret",
|
||||
input: fmt.Sprintf("tableau %s\nserver=%s", validPATName, validTableauURL),
|
||||
want: []string{},
|
||||
description: "Tests that missing secret produces no results",
|
||||
},
|
||||
{
|
||||
name: "no_tableau_keywords",
|
||||
input: "username=test\npassword=secret123\nhost=example.com",
|
||||
want: []string{},
|
||||
description: "Tests that non-Tableau config produces no results",
|
||||
},
|
||||
{
|
||||
name: "token_name_with_whitespace",
|
||||
input: fmt.Sprintf("name %s\n%s\nserver=%s", validPATName, validPATSecret, validTableauURL),
|
||||
want: []string{fmt.Sprintf("%s:%s:%s", validPATName, validPATSecret, validTableauURL)},
|
||||
description: "Tests token name with extra whitespace",
|
||||
},
|
||||
{
|
||||
name: "pat_with_single_quotes",
|
||||
input: fmt.Sprintf("pat '%s'\n%s\nserver=%s", validPATName, validPATSecret, validTableauURL),
|
||||
want: []string{fmt.Sprintf("%s:%s:%s", validPATName, validPATSecret, validTableauURL)},
|
||||
description: "Tests token name with single quotes",
|
||||
},
|
||||
}
|
||||
|
||||
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("keywords '%v' not matched in input: %s", d.Keywords(), test.input)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(matchedDetectors) == 0 && len(test.want) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
results, err := d.FromData(context.Background(), false, []byte(test.input))
|
||||
if err != nil {
|
||||
t.Errorf("FromData error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(results) != len(test.want) {
|
||||
t.Errorf("expected %d results, got %d", len(test.want), len(results))
|
||||
return
|
||||
}
|
||||
|
||||
actual := make(map[string]struct{}, len(results))
|
||||
for _, r := range results {
|
||||
actual[string(r.RawV2)] = 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -725,6 +725,7 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/survicate"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/swell"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/swiftype"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/tableau"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/tailscale"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/tallyfy"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/tatumio"
|
||||
@@ -1723,6 +1724,10 @@ func buildDetectorList() []detectors.Detector {
|
||||
&zohocrm.Scanner{},
|
||||
&zonkafeedback.Scanner{},
|
||||
&zulipchat.Scanner{},
|
||||
&stripepaymentintent.Scanner{},
|
||||
&tableau.Scanner{},
|
||||
&bitbucketapppassword.Scanner{},
|
||||
&hasura.Scanner{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1228,7 +1228,7 @@ func TestEngineInitializesCloudProviderDetectors(t *testing.T) {
|
||||
for _, det := range e.detectors {
|
||||
if endpoints, ok := det.(interface{ Endpoints(...string) []string }); ok {
|
||||
id := config.GetDetectorID(det)
|
||||
if len(endpoints.Endpoints()) == 0 && det.Type() != detectorspb.DetectorType_ArtifactoryAccessToken { // artifactory does not have any cloud endpoint
|
||||
if len(endpoints.Endpoints()) == 0 && det.Type() != detectorspb.DetectorType_ArtifactoryAccessToken && det.Type() != detectorspb.DetectorType_TableauPersonalAccessToken { // artifactory and tableau does not have any cloud endpoint
|
||||
t.Fatalf("detector %q Endpoints() is empty", id.String())
|
||||
}
|
||||
count++
|
||||
|
||||
@@ -1140,6 +1140,7 @@ const (
|
||||
DetectorType_SalesforceRefreshToken DetectorType = 1031
|
||||
DetectorType_AnypointOAuth2 DetectorType = 1032
|
||||
DetectorType_WebexBot DetectorType = 1033
|
||||
DetectorType_TableauPersonalAccessToken DetectorType = 1034
|
||||
)
|
||||
|
||||
// Enum value maps for DetectorType.
|
||||
@@ -2175,6 +2176,7 @@ var (
|
||||
1031: "SalesforceRefreshToken",
|
||||
1032: "AnypointOAuth2",
|
||||
1033: "WebexBot",
|
||||
1034: "TableauPersonalAccessToken",
|
||||
}
|
||||
DetectorType_value = map[string]int32{
|
||||
"Alibaba": 0,
|
||||
@@ -3207,6 +3209,7 @@ var (
|
||||
"SalesforceRefreshToken": 1031,
|
||||
"AnypointOAuth2": 1032,
|
||||
"WebexBot": 1033,
|
||||
"TableauPersonalAccessToken": 1034,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3660,7 +3663,7 @@ var file_detectors_proto_rawDesc = []byte{
|
||||
0x4c, 0x41, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x41, 0x53, 0x45, 0x36, 0x34,
|
||||
0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x55, 0x54, 0x46, 0x31, 0x36, 0x10, 0x03, 0x12, 0x13, 0x0a,
|
||||
0x0f, 0x45, 0x53, 0x43, 0x41, 0x50, 0x45, 0x44, 0x5f, 0x55, 0x4e, 0x49, 0x43, 0x4f, 0x44, 0x45,
|
||||
0x10, 0x04, 0x2a, 0xcb, 0x85, 0x01, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72,
|
||||
0x10, 0x04, 0x2a, 0xec, 0x85, 0x01, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72,
|
||||
0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x10,
|
||||
0x00, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x4d, 0x51, 0x50, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x41,
|
||||
0x57, 0x53, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x10, 0x03, 0x12,
|
||||
@@ -4729,11 +4732,13 @@ var file_detectors_proto_rawDesc = []byte{
|
||||
0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0x87, 0x08, 0x12, 0x13, 0x0a,
|
||||
0x0e, 0x41, 0x6e, 0x79, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x4f, 0x41, 0x75, 0x74, 0x68, 0x32, 0x10,
|
||||
0x88, 0x08, 0x12, 0x0d, 0x0a, 0x08, 0x57, 0x65, 0x62, 0x65, 0x78, 0x42, 0x6f, 0x74, 0x10, 0x89,
|
||||
0x08, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
|
||||
0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f,
|
||||
0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f, 0x67, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b,
|
||||
0x67, 0x2f, 0x70, 0x62, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x70, 0x62,
|
||||
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x08, 0x12, 0x1f, 0x0a, 0x1a, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x61, 0x75, 0x50, 0x65, 0x72, 0x73,
|
||||
0x6f, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x10,
|
||||
0x8a, 0x08, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79,
|
||||
0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f, 0x67, 0x2f, 0x76, 0x33, 0x2f, 0x70,
|
||||
0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x70,
|
||||
0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -1043,6 +1043,7 @@ enum DetectorType {
|
||||
SalesforceRefreshToken = 1031;
|
||||
AnypointOAuth2 = 1032;
|
||||
WebexBot = 1033;
|
||||
TableauPersonalAccessToken = 1034;
|
||||
}
|
||||
|
||||
message Result {
|
||||
|
||||
Reference in New Issue
Block a user