diff --git a/pkg/detectors/basicauth/basicauth.go b/pkg/detectors/basicauth/basicauth.go new file mode 100644 index 000000000000..c8faa73e307d --- /dev/null +++ b/pkg/detectors/basicauth/basicauth.go @@ -0,0 +1,89 @@ +package basicauth + +import ( + "context" + "encoding/base64" + "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{} + +var ( + // Ensure the Scanner satisfies the interface at compile time. + _ detectors.Detector = (*Scanner)(nil) + + // Pattern matches Authorization: Basic or similar variations + // The base64 part should contain at least one colon when decoded (username:password format) + keyPat = regexp.MustCompile(`(?i)(?:authorization|auth)[\s:=]+basic[\s]+([A-Za-z0-9+/]{20,}={0,2})`) +) + +// Keywords are used for efficiently pre-filtering chunks. +func (s Scanner) Keywords() []string { + return []string{"authorization", "basic", "auth"} +} + +// FromData will find and optionally verify BasicAuth secrets in a given set of bytes. +func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { + dataStr := string(data) + + matches := keyPat.FindAllStringSubmatch(dataStr, -1) + + for _, match := range matches { + resMatch := strings.TrimSpace(match[1]) + + // Decode base64 to verify it contains username:password format + decoded, err := base64.StdEncoding.DecodeString(resMatch) + if err != nil { + // Try URL encoding as fallback + decoded, err = base64.URLEncoding.DecodeString(resMatch) + if err != nil { + continue + } + } + + decodedStr := string(decoded) + + // Basic auth must contain at least one colon separating username and password + if !strings.Contains(decodedStr, ":") { + continue + } + + // Split to get username and password + parts := strings.SplitN(decodedStr, ":", 2) + if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 { + continue + } + + s1 := detectors.Result{ + DetectorType: detector_typepb.DetectorType_BasicAuth, + Raw: []byte(resMatch), + RawV2: []byte(decodedStr), + SecretParts: map[string]string{ + "username": parts[0], + "password": parts[1], + "encoded": resMatch, + }, + } + + // Basic auth tokens cannot be verified without knowing the target URL/endpoint + // So we mark them as unverified by default + s1.Verified = false + + results = append(results, s1) + } + + return results, nil +} + +func (s Scanner) Type() detector_typepb.DetectorType { + return detector_typepb.DetectorType_BasicAuth +} + +func (s Scanner) Description() string { + return "HTTP Basic Authentication is a simple authentication scheme built into the HTTP protocol. Basic Auth credentials consist of a username and password encoded in base64 format." +} diff --git a/pkg/detectors/basicauth/basicauth_test.go b/pkg/detectors/basicauth/basicauth_test.go new file mode 100644 index 000000000000..76109a5e202f --- /dev/null +++ b/pkg/detectors/basicauth/basicauth_test.go @@ -0,0 +1,156 @@ +package basicauth + +import ( + "context" + "encoding/base64" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" + "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb" +) + +func TestBasicAuth_FromChunk(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*1000000000) // 5 seconds + defer cancel() + + // Create test credentials + validCreds := base64.StdEncoding.EncodeToString([]byte("admin:password123")) + validCredsComplex := base64.StdEncoding.EncodeToString([]byte("user@example.com:P@ssw0rd!2023")) + invalidNoCreds := base64.StdEncoding.EncodeToString([]byte("justonefield")) + invalidEmptyPassword := base64.StdEncoding.EncodeToString([]byte("username:")) + + tests := []struct { + name string + input string + want []detectors.Result + wantErr bool + wantMatches int + }{ + { + name: "valid basic auth with Authorization header", + input: fmt.Sprintf("Authorization: Basic %s", validCreds), + wantMatches: 1, + want: []detectors.Result{ + { + DetectorType: detector_typepb.DetectorType_BasicAuth, + Verified: false, + Raw: []byte(validCreds), + RawV2: []byte("admin:password123"), + SecretParts: map[string]string{ + "username": "admin", + "password": "password123", + "encoded": validCreds, + }, + }, + }, + }, + { + name: "valid basic auth with auth header lowercase", + input: fmt.Sprintf("auth: basic %s", validCreds), + wantMatches: 1, + }, + { + name: "valid basic auth with complex password", + input: fmt.Sprintf("Authorization: Basic %s", validCredsComplex), + wantMatches: 1, + want: []detectors.Result{ + { + DetectorType: detector_typepb.DetectorType_BasicAuth, + Verified: false, + Raw: []byte(validCredsComplex), + RawV2: []byte("user@example.com:P@ssw0rd!2023"), + SecretParts: map[string]string{ + "username": "user@example.com", + "password": "P@ssw0rd!2023", + "encoded": validCredsComplex, + }, + }, + }, + }, + { + name: "basic auth with equals separator", + input: fmt.Sprintf("Authorization=Basic %s", validCreds), + wantMatches: 1, + }, + { + name: "basic auth in curl command", + input: fmt.Sprintf("curl -H 'Authorization: Basic %s' https://api.example.com", validCreds), + wantMatches: 1, + }, + { + name: "invalid - no colon separator", + input: fmt.Sprintf("Authorization: Basic %s", invalidNoCreds), + wantMatches: 0, + }, + { + name: "invalid - empty password", + input: fmt.Sprintf("Authorization: Basic %s", invalidEmptyPassword), + wantMatches: 0, + }, + { + name: "invalid - not base64", + input: "Authorization: Basic not-valid-base64!!!", + wantMatches: 0, + }, + { + name: "invalid - too short", + input: "Authorization: Basic YWRt", + wantMatches: 0, + }, + { + name: "multiple basic auth tokens", + input: fmt.Sprintf("Authorization: Basic %s\nAuthorization: Basic %s", validCreds, validCredsComplex), + wantMatches: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := Scanner{} + got, err := s.FromData(ctx, false, []byte(tt.input)) + if (err != nil) != tt.wantErr { + t.Errorf("BasicAuth.FromData() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if len(got) != tt.wantMatches { + t.Errorf("BasicAuth.FromData() got %d matches, want %d", len(got), tt.wantMatches) + return + } + + if tt.want != nil && len(got) > 0 { + // Compare first result + ignoreOpts := cmpopts.IgnoreUnexported(detectors.Result{}) + + if diff := cmp.Diff(got[0], tt.want[0], ignoreOpts); diff != "" { + t.Errorf("BasicAuth.FromData() mismatch (-got +want):\n%s", diff) + } + } + }) + } +} + +func TestBasicAuth_Keywords(t *testing.T) { + s := Scanner{} + keywords := s.Keywords() + + if len(keywords) == 0 { + t.Error("Keywords() returned empty slice") + } + + expectedKeywords := map[string]bool{ + "authorization": true, + "basic": true, + "auth": true, + } + + for _, kw := range keywords { + if !expectedKeywords[kw] { + t.Errorf("unexpected keyword: %s", kw) + } + } +} diff --git a/pkg/detectors/streamio/streamio.go b/pkg/detectors/streamio/streamio.go new file mode 100644 index 000000000000..8997b0628cbd --- /dev/null +++ b/pkg/detectors/streamio/streamio.go @@ -0,0 +1,214 @@ +package streamio + +import ( + "context" + "fmt" + "io" + "net/http" + + "github.com/golang-jwt/jwt/v5" + 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 { + client *http.Client + detectors.DefaultMultiPartCredentialProvider +} + +var ( + _ detectors.Detector = (*Scanner)(nil) + + defaultClient = common.SaneHttpClient() + + // Stream.io requires App ID, API Key, and API Secret + // App ID format: numeric, typically 5-10 digits + // API Key format: alphanumeric, typically 8-20 characters + // API Secret format: alphanumeric, typically 40-80 characters + appIdPat = regexp.MustCompile(detectors.PrefixRegex([]string{"stream", "app.?id", "getstream"}) + `\b([0-9]{5,10})\b`) + keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"stream", "api.?key", "getstream"}) + `\b([a-z0-9]{8,20})\b`) + secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"stream", "api.?secret", "getstream"}) + `\b([a-z0-9]{40,80})\b`) +) + +// Keywords are used for efficiently pre-filtering chunks. +func (s Scanner) Keywords() []string { + return []string{"stream", "getstream", "stream.io"} +} + +func (s Scanner) getClient() *http.Client { + if s.client != nil { + return s.client + } + return defaultClient +} + +// FromData will find and optionally verify Stream.io secrets in a given set of bytes. +func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { + dataStr := string(data) + + uniqueAppIds := make(map[string]struct{}) + uniqueKeys := make(map[string]struct{}) + uniqueSecrets := make(map[string]struct{}) + + for _, match := range appIdPat.FindAllStringSubmatch(dataStr, -1) { + uniqueAppIds[match[1]] = struct{}{} + } + + for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) { + uniqueKeys[match[1]] = struct{}{} + } + + for _, match := range secretPat.FindAllStringSubmatch(dataStr, -1) { + uniqueSecrets[match[1]] = struct{}{} + } + + // If no app IDs found, add empty string so we can still detect key+secret pairs + if len(uniqueAppIds) == 0 { + uniqueAppIds[""] = struct{}{} + } + + for appId := range uniqueAppIds { + for apiKey := range uniqueKeys { + for apiSecret := range uniqueSecrets { + secretParts := map[string]string{ + "api_key": apiKey, + "api_secret": apiSecret, + } + if appId != "" { + secretParts["app_id"] = appId + } + + s1 := detectors.Result{ + DetectorType: detector_typepb.DetectorType_StreamIO, + Raw: []byte(apiKey), + RawV2: []byte(appId + apiKey + apiSecret), + SecretParts: secretParts, + } + + if verify { + isVerified, verificationErr := verifyStreamIO(ctx, s.getClient(), appId, apiKey, apiSecret) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, apiSecret) + } + + results = append(results, s1) + } + } + } + + return results, nil +} + +func verifyStreamIO(ctx context.Context, client *http.Client, appId, apiKey, apiSecret string) (bool, error) { + // GetStream.io requires JWT tokens for authentication + // Generate a token with feed access claims + token, err := generateStreamToken(apiSecret) + if err != nil { + return false, fmt.Errorf("failed to generate token: %w", err) + } + + // Try different regional endpoints + locations := []string{ + "us-east", + "eu-west", + "singapore", + "sydney", + "tokyo", + } + + var lastErr error + var lastStatus int + + for _, location := range locations { + // Try accessing a feed endpoint to verify credentials + // We use a generic feed path - the feed may not exist but auth will be checked first + url := fmt.Sprintf("https://%s-api.stream-io-api.com/api/v1.0/feed/flat/verify/?api_key=%s", + location, apiKey) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + lastErr = err + continue + } + + // Set authorization header with JWT token + req.Header.Set("Authorization", token) + req.Header.Set("Stream-Auth-Type", "jwt") + + res, err := client.Do(req) + if err != nil { + // DNS or network error, try next location + lastErr = err + continue + } + + bodyBytes, _ := io.ReadAll(res.Body) + _ = res.Body.Close() + lastStatus = res.StatusCode + + switch res.StatusCode { + case http.StatusOK: + // Valid credentials and feed exists + return true, nil + case http.StatusBadRequest: + // Feed doesn't exist but auth succeeded (400 = authenticated but bad request) + // This confirms valid credentials + return true, nil + case http.StatusUnauthorized, http.StatusForbidden: + // Invalid credentials + // Check if all regions give same error - if yes, credentials are definitely invalid + bodyStr := string(bodyBytes) + lastErr = fmt.Errorf("auth failed (status %d): %s", res.StatusCode, bodyStr) + continue + case http.StatusNotFound: + // Endpoint not found at this location, try next + lastErr = fmt.Errorf("endpoint not found at %s", location) + continue + default: + // Other status codes (might indicate successful auth but other issues) + lastErr = fmt.Errorf("status %d from %s: %s", res.StatusCode, location, string(bodyBytes)) + continue + } + } + + // If we consistently get 401/403 across all regions, credentials are invalid + if lastStatus == http.StatusUnauthorized || lastStatus == http.StatusForbidden { + return false, lastErr + } + + // Otherwise, couldn't verify (network/config issues) + if lastErr != nil { + return false, lastErr + } + return false, fmt.Errorf("all locations failed") +} + +// generateStreamToken creates a JWT token for Stream.io authentication +func generateStreamToken(apiSecret string) (string, error) { + // Stream.io uses HS256 for signing + // Include resource claims for feed access + claims := jwt.MapClaims{ + "resource": "feed", + "action": "*", + "feed_id": "*", + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(apiSecret)) + if err != nil { + return "", err + } + + return tokenString, nil +} + +func (s Scanner) Type() detector_typepb.DetectorType { + return detector_typepb.DetectorType_StreamIO +} + +func (s Scanner) Description() string { + return "Stream (GetStream.io) is a scalable feed and chat API service. Stream API keys and secrets can be used to authenticate and access chat, activity feeds, and other Stream services." +} diff --git a/pkg/detectors/streamio/streamio_integration_test.go b/pkg/detectors/streamio/streamio_integration_test.go new file mode 100644 index 000000000000..8d9f29d75833 --- /dev/null +++ b/pkg/detectors/streamio/streamio_integration_test.go @@ -0,0 +1,157 @@ +package streamio + +import ( + "context" + "testing" +) + +// TestStreamIO_RealCredentials tests detection and verification with real Stream.io credentials +// INSTRUCTIONS TO USE: +// 1. Get real Stream.io credentials from https://getstream.io/dashboard/ +// 2. Replace the placeholder values below +// 3. Run: go test -v -run TestStreamIO_RealCredentials +func TestStreamIO_RealCredentials(t *testing.T) { + if testing.Short() { + t.Skip("Skipping real credentials test in short mode") + } + + // REPLACE THESE WITH YOUR REAL STREAM.IO CREDENTIALS FROM DASHBOARD + realAppId := "1644228" // Numeric ID from dashboard + realApiKey := "4u3ncebvw27r" + realApiSecret := "as57ayreare6wqz2vj2uvsgcmpgbjejcchkdd9723gku26dqvwezgpbwnbpwmsn7" + + // Skip if using placeholder values (commented out for testing) + if realAppId == "your_app_id_here" { + t.Skip("Replace realAppId, realApiKey, and realApiSecret with actual Stream.io credentials to run this test") + } + + ctx := context.Background() + scanner := Scanner{} + + // Create test input with real credentials + input := ` +stream_app_id=` + realAppId + ` +stream_api_key=` + realApiKey + ` +stream_api_secret=` + realApiSecret + + t.Logf("Testing with real Stream.io credentials") + t.Logf("App ID: %s", realAppId) + t.Logf("API Key: %s", realApiKey) + t.Logf("API Secret: %s***", realApiSecret[:10]) // Only show first 10 chars + + // Test detection (without verification) + t.Run("Detection", func(t *testing.T) { + results, err := scanner.FromData(ctx, false, []byte(input)) + if err != nil { + t.Fatalf("Error during detection: %v", err) + } + + if len(results) == 0 { + t.Fatal("No credentials detected!") + } + + result := results[0] + t.Logf("✅ Detection successful!") + t.Logf(" App ID extracted: %s", result.SecretParts["app_id"]) + t.Logf(" API Key extracted: %s", result.SecretParts["api_key"]) + t.Logf(" API Secret extracted: %s***", result.SecretParts["api_secret"][:10]) + + if result.SecretParts["app_id"] != realAppId { + t.Errorf("App ID mismatch: got %q, want %q", result.SecretParts["app_id"], realAppId) + } + if result.SecretParts["api_key"] != realApiKey { + t.Errorf("API Key mismatch: got %q, want %q", result.SecretParts["api_key"], realApiKey) + } + if result.SecretParts["api_secret"] != realApiSecret { + t.Errorf("API Secret mismatch: got %q, want %q", result.SecretParts["api_secret"], realApiSecret) + } + }) + + // Test verification (with actual API call) + t.Run("Verification", func(t *testing.T) { + results, err := scanner.FromData(ctx, true, []byte(input)) + if err != nil { + t.Fatalf("Error during verification: %v", err) + } + + if len(results) == 0 { + t.Fatal("No credentials detected!") + } + + result := results[0] + t.Logf("Verification result: Verified=%v", result.Verified) + + if result.Verified { + t.Logf("✅ Credentials verified successfully!") + } else { + t.Logf("⚠️ Credentials not verified (either invalid or verification endpoint issue)") + if result.VerificationError() != nil { + t.Logf(" Verification error: %v", result.VerificationError()) + } + } + + // Note: We don't fail the test if verification fails, as it might be due to: + // - Network issues + // - Rate limiting + // - Endpoint changes + // The important part is that detection worked + }) +} + +// TestStreamIO_DetectionOnly tests only the detection logic with sample credentials +// This runs even without real credentials +func TestStreamIO_DetectionOnly(t *testing.T) { + ctx := context.Background() + scanner := Scanner{} + + testCases := []struct { + name string + apiKey string + apiSecret string + }{ + { + name: "Short format", + apiKey: "abcd1234", + apiSecret: "secret1234567890abcdefghijklmnopqrstuvwxyz12", + }, + { + name: "Medium format", + apiKey: "streamkey12345", + apiSecret: "streamsecret1234567890abcdefghijklmnopqrstuvwxyz123456", + }, + { + name: "Long format", + apiKey: "mystreamkey123456789", + apiSecret: "mystreamsecret1234567890abcdefghijklmnopqrstuvwxyz1234567890abcdef", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + input := ` +stream_api_key=` + tc.apiKey + ` +stream_api_secret=` + tc.apiSecret + + results, err := scanner.FromData(ctx, false, []byte(input)) + if err != nil { + t.Fatalf("Error: %v", err) + } + + if len(results) == 0 { + t.Fatal("Expected to find credentials, but got none") + } + + result := results[0] + if result.SecretParts["api_key"] != tc.apiKey { + t.Errorf("API Key mismatch: got %q, want %q", result.SecretParts["api_key"], tc.apiKey) + } + if result.SecretParts["api_secret"] != tc.apiSecret { + t.Errorf("API Secret mismatch: got %q, want %q", result.SecretParts["api_secret"], tc.apiSecret) + } + + t.Logf("✅ Successfully detected: key=%s, secret=%s***", + result.SecretParts["api_key"], + result.SecretParts["api_secret"][:10]) + }) + } +} diff --git a/pkg/detectors/streamio/streamio_test.go b/pkg/detectors/streamio/streamio_test.go new file mode 100644 index 000000000000..d3d6caa604e3 --- /dev/null +++ b/pkg/detectors/streamio/streamio_test.go @@ -0,0 +1,226 @@ +package streamio + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" + "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb" +) + +func TestStreamIO_FromChunk(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*1000000000) // 5 seconds + defer cancel() + + tests := []struct { + name string + input string + want []detectors.Result + wantErr bool + wantMatches int + }{ + { + name: "valid stream api key and secret", + input: ` +stream_api_key=abcd1234efgh +stream_api_secret=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 +`, + wantMatches: 1, + want: []detectors.Result{ + { + DetectorType: detector_typepb.DetectorType_StreamIO, + Verified: false, + SecretParts: map[string]string{ + "api_key": "abcd1234efgh", + "api_secret": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0", + }, + }, + }, + }, + { + name: "valid with getstream prefix", + input: ` +getstream_key=testkey12345 +getstream_secret=secret1234567890abcdefghijklmnopqrstuvwxyz1234567890 +`, + wantMatches: 1, + }, + { + name: "valid in environment variables", + input: ` +STREAM_API_KEY=myapikey123 +STREAM_API_SECRET=mysecretkey1234567890abcdefghijklmnopqrstuvwxyz12345 +`, + wantMatches: 1, + }, + { + name: "valid in JSON config", + input: `{ + "stream": { + "api_key": "streamkey123", + "api_secret": "streamsecret1234567890abcdefghijklmnopqrstuvwxyz" + } +}`, + wantMatches: 1, + }, + { + name: "valid in code", + input: ` +const streamApiKey = "appkey12345" +const streamApiSecret = "appsecret1234567890abcdefghijklmnopqrstuvwxyz123" +`, + wantMatches: 1, + }, + { + name: "invalid - key too short", + input: ` +stream_api_key=short +stream_api_secret=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 +`, + wantMatches: 0, + }, + { + name: "invalid - secret too short", + input: ` +stream_api_key=validkey123 +stream_api_secret=tooshort +`, + wantMatches: 0, + }, + { + name: "invalid - key only without secret", + input: ` +stream_api_key=validkey123 +`, + wantMatches: 0, + }, + { + name: "invalid - secret only without key", + input: ` +stream_api_secret=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 +`, + wantMatches: 0, + }, + { + name: "multiple valid key-secret pairs", + input: ` +stream_api_key=firstkey123 +stream_api_secret=firstsecret1234567890abcdefghijklmnopqrstuvwxyz +stream_api_key=secondkey456 +stream_api_secret=secondsecret1234567890abcdefghijklmnopqrstuvwxy +`, + wantMatches: 4, // 2 keys x 2 secrets = 4 combinations + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := Scanner{} + got, err := s.FromData(ctx, false, []byte(tt.input)) + if (err != nil) != tt.wantErr { + t.Errorf("StreamIO.FromData() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if len(got) != tt.wantMatches { + t.Errorf("StreamIO.FromData() got %d matches, want %d", len(got), tt.wantMatches) + return + } + + if tt.want != nil && len(got) > 0 { + ignoreOpts := cmpopts.IgnoreUnexported(detectors.Result{}) + + if diff := cmp.Diff(got[0].SecretParts, tt.want[0].SecretParts); diff != "" { + t.Errorf("StreamIO.FromData() SecretParts mismatch (-got +want):\n%s", diff) + } + + if got[0].DetectorType != tt.want[0].DetectorType { + t.Errorf("StreamIO.FromData() DetectorType = %v, want %v", got[0].DetectorType, tt.want[0].DetectorType) + } + + _ = ignoreOpts // ignore unused warning + } + }) + } +} + +func TestStreamIO_Keywords(t *testing.T) { + s := Scanner{} + keywords := s.Keywords() + + if len(keywords) == 0 { + t.Error("Keywords() returned empty slice") + } + + expectedKeywords := map[string]bool{ + "stream": true, + "getstream": true, + "stream.io": true, + } + + for _, kw := range keywords { + if !expectedKeywords[kw] { + t.Errorf("unexpected keyword: %s", kw) + } + } +} + +// TestStreamIO_Pattern tests the regex patterns +func TestStreamIO_Pattern(t *testing.T) { + tests := []struct { + name string + input string + shouldMatch bool + matchType string // "key" or "secret" + }{ + { + name: "valid api key", + input: "stream_api_key=abcdef1234", + shouldMatch: true, + matchType: "key", + }, + { + name: "valid api secret", + input: "stream_api_secret=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0", + shouldMatch: true, + matchType: "secret", + }, + { + name: "key with uppercase in value should not match", + input: "stream_api_key=ABCDEF1234", + shouldMatch: false, + matchType: "key", + }, + { + name: "key too short", + input: "stream_api_key=short", + shouldMatch: false, + matchType: "key", + }, + { + name: "secret too short", + input: "stream_api_secret=short", + shouldMatch: false, + matchType: "secret", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var matches [][]string + if tt.matchType == "key" { + matches = keyPat.FindAllStringSubmatch(tt.input, -1) + } else { + matches = secretPat.FindAllStringSubmatch(tt.input, -1) + } + + matched := len(matches) > 0 + if matched != tt.shouldMatch { + t.Errorf("Pattern match = %v, want %v for input: %s", matched, tt.shouldMatch, tt.input) + } + }) + } +} diff --git a/pkg/engine/defaults/defaults.go b/pkg/engine/defaults/defaults.go index f3604c038def..a3bb98f4f9bf 100644 --- a/pkg/engine/defaults/defaults.go +++ b/pkg/engine/defaults/defaults.go @@ -91,6 +91,7 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azuresastoken" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azuresearchadminkey" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azuresearchquerykey" + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/basicauth" bannerbearv1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/bannerbear/v1" bannerbearv2 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/bannerbear/v2" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/baremetrics" @@ -969,6 +970,7 @@ func buildDetectorList() []detectors.Detector { &azuresearchquerykey.Scanner{}, &azure_storage.Scanner{}, &azurerepositorykey.Scanner{}, + &basicauth.Scanner{}, &bannerbearv1.Scanner{}, &bannerbearv2.Scanner{}, &baremetrics.Scanner{}, diff --git a/pkg/pb/detector_typepb/detector_type.pb.go b/pkg/pb/detector_typepb/detector_type.pb.go index 11428679df44..c64b091ef08b 100644 --- a/pkg/pb/detector_typepb/detector_type.pb.go +++ b/pkg/pb/detector_typepb/detector_type.pb.go @@ -1106,6 +1106,8 @@ const ( DetectorType_GitLabOauth2 DetectorType = 1050 DetectorType_SpectralOps DetectorType = 1051 DetectorType_AWSAppSync DetectorType = 1052 + DetectorType_BasicAuth DetectorType = 1057 + DetectorType_StreamIO DetectorType = 1058 ) // Enum value maps for DetectorType. @@ -2160,6 +2162,8 @@ var ( 1050: "GitLabOauth2", 1051: "SpectralOps", 1052: "AWSAppSync", + 1057: "BasicAuth", + 1058: "StreamIO", } DetectorType_value = map[string]int32{ "Alibaba": 0, @@ -3211,6 +3215,8 @@ var ( "GitLabOauth2": 1050, "SpectralOps": 1051, "AWSAppSync": 1052, + "BasicAuth": 1057, + "StreamIO": 1058, } ) diff --git a/proto/detector_type.proto b/proto/detector_type.proto index 6423f9ede368..19d28d36a9ce 100644 --- a/proto/detector_type.proto +++ b/proto/detector_type.proto @@ -1054,4 +1054,6 @@ enum DetectorType { GitLabOauth2 = 1050; SpectralOps = 1051; AWSAppSync = 1052; + BasicAuth = 1057; + StreamIO = 1058; }