From d230450e11c73633cdefd0ec08729b34422eae6c Mon Sep 17 00:00:00 2001 From: Apoorv Munshi Date: Fri, 5 Sep 2025 09:39:23 -0700 Subject: [PATCH 1/4] changes to confluent detector for new secret pattern --- pkg/detectors/confluent/{ => v1}/confluent.go | 6 + .../{ => v1}/confluent_integration_test.go | 0 .../confluent/{ => v1}/confluent_test.go | 0 pkg/detectors/confluent/v2/confluent.go | 111 ++++++++++++++++ .../v2/confluent_integration_test.go | 122 ++++++++++++++++++ pkg/detectors/confluent/v2/confluent_test.go | 92 +++++++++++++ 6 files changed, 331 insertions(+) rename pkg/detectors/confluent/{ => v1}/confluent.go (89%) rename pkg/detectors/confluent/{ => v1}/confluent_integration_test.go (100%) rename pkg/detectors/confluent/{ => v1}/confluent_test.go (100%) create mode 100644 pkg/detectors/confluent/v2/confluent.go create mode 100644 pkg/detectors/confluent/v2/confluent_integration_test.go create mode 100644 pkg/detectors/confluent/v2/confluent_test.go diff --git a/pkg/detectors/confluent/confluent.go b/pkg/detectors/confluent/v1/confluent.go similarity index 89% rename from pkg/detectors/confluent/confluent.go rename to pkg/detectors/confluent/v1/confluent.go index 6283e8f7c227..a44976b5b12c 100644 --- a/pkg/detectors/confluent/confluent.go +++ b/pkg/detectors/confluent/v1/confluent.go @@ -35,6 +35,8 @@ func (s Scanner) Keywords() []string { return []string{"confluent"} } +func (Scanner) Version() int { return 1 } + // FromData will find and optionally verify Confluent 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) @@ -52,6 +54,10 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result DetectorType: detectorspb.DetectorType_Confluent, Raw: []byte(resMatch), RawV2: []byte(resMatch + resSecret), + ExtraData: map[string]string{ + "rotation_guide": "https://docs.confluent.io/cloud/current/security/authenticate/workload-identities/service-accounts/api-keys/best-practices-api-keys.html#rotate-api-keys-regularly", + "version": fmt.Sprintf("%d", s.Version()), + }, } if verify { diff --git a/pkg/detectors/confluent/confluent_integration_test.go b/pkg/detectors/confluent/v1/confluent_integration_test.go similarity index 100% rename from pkg/detectors/confluent/confluent_integration_test.go rename to pkg/detectors/confluent/v1/confluent_integration_test.go diff --git a/pkg/detectors/confluent/confluent_test.go b/pkg/detectors/confluent/v1/confluent_test.go similarity index 100% rename from pkg/detectors/confluent/confluent_test.go rename to pkg/detectors/confluent/v1/confluent_test.go diff --git a/pkg/detectors/confluent/v2/confluent.go b/pkg/detectors/confluent/v2/confluent.go new file mode 100644 index 000000000000..f13f9445abf9 --- /dev/null +++ b/pkg/detectors/confluent/v2/confluent.go @@ -0,0 +1,111 @@ +package confluent + +import ( + "context" + b64 "encoding/base64" + "fmt" + "hash/crc32" + "strings" + + regexp "github.com/wasilibs/go-re2" + + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" + "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" +) + +type Scanner struct { + detectors.DefaultMultiPartCredentialProvider +} + +// Ensure the Scanner satisfies the interface at compile time. +var _ detectors.Detector = (*Scanner)(nil) + +var ( + // Match cflt prefix followed by 60 characters consisting of A-Z, a-z, 0-9, + or / + //See https://docs.confluent.io/cloud/current/security/authenticate/workload-identities/service-accounts/api-keys/overview.html#api-secret-format + secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"Confluent"}) + `\b(cflt[A-Za-z0-9+/]{60})\b`) +) + +// Keywords are used for efficiently pre-filtering chunks. +// Use identifiers in the secret preferably, or the provider name. +func (s Scanner) Keywords() []string { + return []string{"cflt"} +} + +func (Scanner) Version() int { return 2 } + +// FromData will find and optionally verify Confluent 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) + + secretMatches := secretPat.FindAllStringSubmatch(dataStr, -1) + + for _, match := range secretMatches { + resSecret := strings.TrimSpace(match[1]) // Use index 1 for the captured group + + s1 := detectors.Result{ + DetectorType: detectorspb.DetectorType_Confluent, + Raw: []byte(resSecret), + ExtraData: map[string]string{ + "rotation_guide": "https://docs.confluent.io/cloud/current/security/authenticate/workload-identities/service-accounts/api-keys/best-practices-api-keys.html#rotate-api-keys-regularly", + "version": fmt.Sprintf("%d", s.Version()), + }, + } + + if verify { + s1.Verified = verifyConfluentSecret(resSecret) + } + + results = append(results, s1) + } + + return results, nil +} + +// verifyConfluentSecret verifies the Confluent secret by checking the CRC32 checksum +func verifyConfluentSecret(secret string) bool { + if len(secret) != 64 { // cflt + 60 characters + return false + } + + if !strings.HasPrefix(secret, "cflt") { + return false + } + + // Extract the first 54 characters after 'cflt' prefix (58 total - 4 for cflt) + payload := secret[4:58] // Characters 5-58 (54 characters) + + // Extract the last 6 characters as the checksum + checksumEncoded := secret[58:64] + + // Decode the checksum from base64 + checksumBytes, err := b64.StdEncoding.DecodeString(checksumEncoded + "==") // Add padding if needed + if err != nil { + // Try without padding + checksumBytes, err = b64.StdEncoding.DecodeString(checksumEncoded) + if err != nil { + return false + } + } + + if len(checksumBytes) < 4 { + return false + } + + // Calculate CRC32 checksum of the payload + expectedChecksum := crc32.ChecksumIEEE([]byte(payload)) + + // Convert received checksum bytes to uint32 (little endian to match the encoding) + receivedChecksum := uint32(checksumBytes[3])<<24 | uint32(checksumBytes[2])<<16 | + uint32(checksumBytes[1])<<8 | uint32(checksumBytes[0]) + + return expectedChecksum == receivedChecksum +} + +func (s Scanner) Type() detectorspb.DetectorType { + return detectorspb.DetectorType_Confluent +} + +func (s Scanner) Description() string { + return "Confluent provides a streaming platform based on Apache Kafka to help companies harness their data in real-time. Confluent Cloud API keys can be used to access and manage Confluent Cloud control plane APIs and resources." +} diff --git a/pkg/detectors/confluent/v2/confluent_integration_test.go b/pkg/detectors/confluent/v2/confluent_integration_test.go new file mode 100644 index 000000000000..9e5f98eda39f --- /dev/null +++ b/pkg/detectors/confluent/v2/confluent_integration_test.go @@ -0,0 +1,122 @@ +//go:build detectors +// +build detectors + +package confluent + +import ( + "context" + "fmt" + "testing" + + "github.com/kylelemons/godebug/pretty" + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" + + "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" +) + +func TestConfluent_FromChunk(t *testing.T) { + // Valid secret with proper CRC32 checksum + validSecret := "cfltT8d8RzkNseTMEDKcjNM1BZTFPHqRn/dQm9q7w6SjzZ12wZfwjaJdipHZtDjw" + // Invalid secret with wrong checksum + invalidSecret := "cfltT8d8RzkNseTMEDKcjNM1BZTFPHqRn/dQm9q7w6SjzZ12wZfwjaJdipHZtDji" + + type args struct { + ctx context.Context + data []byte + verify bool + } + tests := []struct { + name string + s Scanner + args args + want []detectors.Result + wantErr bool + }{ + { + name: "found, verified", + s: Scanner{}, + args: args{ + ctx: context.Background(), + data: []byte(fmt.Sprintf("You can find a confluent secret %s", validSecret)), + verify: true, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_Confluent, + Verified: true, + ExtraData: map[string]string{ + "rotation_guide": "https://docs.confluent.io/cloud/current/security/authenticate/workload-identities/service-accounts/api-keys/best-practices-api-keys.html#rotate-api-keys-regularly", + "version": "2", + }, + }, + }, + wantErr: false, + }, + { + name: "found, unverified", + s: Scanner{}, + args: args{ + ctx: context.Background(), + data: []byte(fmt.Sprintf("You can find a confluent secret %s but not valid", invalidSecret)), // the secret would satisfy the regex but not pass validation + verify: true, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_Confluent, + Verified: false, + ExtraData: map[string]string{ + "rotation_guide": "https://docs.confluent.io/cloud/current/security/authenticate/workload-identities/service-accounts/api-keys/best-practices-api-keys.html#rotate-api-keys-regularly", + "version": "2", + }, + }, + }, + wantErr: false, + }, + { + name: "not found", + s: Scanner{}, + args: args{ + ctx: context.Background(), + data: []byte("You cannot find the secret within"), + verify: true, + }, + want: nil, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := Scanner{} + got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data) + if (err != nil) != tt.wantErr { + t.Errorf("Confluent.FromData() error = %v, wantErr %v", err, tt.wantErr) + return + } + for i := range got { + if len(got[i].Raw) == 0 { + t.Fatalf("no raw secret present: \n %+v", got[i]) + } + got[i].Raw = nil + } + if diff := pretty.Compare(got, tt.want); diff != "" { + t.Errorf("Confluent.FromData() %s diff: (-got +want)\n%s", tt.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) + } + } + }) + } +} diff --git a/pkg/detectors/confluent/v2/confluent_test.go b/pkg/detectors/confluent/v2/confluent_test.go new file mode 100644 index 000000000000..dc002453ace4 --- /dev/null +++ b/pkg/detectors/confluent/v2/confluent_test.go @@ -0,0 +1,92 @@ +package confluent + +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 ( + validPattern = ` + # Configuration File: config.yaml + database: + host: $DB_HOST + port: $DB_PORT + username: $DB_USERNAME + password: $DB_PASS # IMPORTANT: Do not share this password publicly + + api: + auth_type: "Basic" + in: "Header" + confluent_secret: "cfltT8d8RzkNseTMEDKcjNM1BZTFPHqRn/dQm9q7w6SjzZ12wZfwjaJdipj0ObHQ" + base_url: "https://api.example.com/v1/user" + + # Notes: + # - Remember to rotate the secret every 90 days. + # - The above credentials should only be used in a secure environment. + ` + secret = "cfltT8d8RzkNseTMEDKcjNM1BZTFPHqRn/dQm9q7w6SjzZ12wZfwjaJdipj0ObHQ" +) + +func TestConfluent_Pattern(t *testing.T) { + d := Scanner{} + ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d}) + + tests := []struct { + name string + input string + want []string + }{ + { + name: "valid pattern", + input: validPattern, + want: []string{secret}, + }, + } + + 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) + } + }) + } +} From 0a3e20d57860464a573b2859b29e21280ca87b6c Mon Sep 17 00:00:00 2001 From: Apoorv Munshi Date: Tue, 7 Oct 2025 11:09:15 -0700 Subject: [PATCH 2/4] add versioned confluent detectors to defaults.go --- pkg/engine/defaults/defaults.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/engine/defaults/defaults.go b/pkg/engine/defaults/defaults.go index 45c03b957d94..ed44dccac2a2 100644 --- a/pkg/engine/defaults/defaults.go +++ b/pkg/engine/defaults/defaults.go @@ -183,7 +183,8 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/commercejs" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/commodities" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/companyhub" - "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/confluent" + confluentv1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/confluent/v1" + confluentv2 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/confluent/v2" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/contentfulpersonalaccesstoken" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/conversiontools" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/convertapi" @@ -1047,7 +1048,8 @@ func buildDetectorList() []detectors.Detector { &commercejs.Scanner{}, &commodities.Scanner{}, &companyhub.Scanner{}, - &confluent.Scanner{}, + &confluentv1.Scanner{}, + &confluentv2.Scanner{}, &contentfulpersonalaccesstoken.Scanner{}, &conversiontools.Scanner{}, &convertapi.Scanner{}, From 8329c670ebaf880e380e6399dbbbd3e9d9a090b1 Mon Sep 17 00:00:00 2001 From: Apoorv Munshi Date: Wed, 8 Oct 2025 11:05:44 -0700 Subject: [PATCH 3/4] add confluent API key detection in V2 detector --- pkg/detectors/confluent/v1/confluent.go | 2 +- pkg/detectors/confluent/v2/confluent.go | 39 +++++++++++-------- .../v2/confluent_integration_test.go | 7 ++-- pkg/detectors/confluent/v2/confluent_test.go | 25 +++++------- 4 files changed, 38 insertions(+), 35 deletions(-) diff --git a/pkg/detectors/confluent/v1/confluent.go b/pkg/detectors/confluent/v1/confluent.go index a44976b5b12c..30a73205ccf6 100644 --- a/pkg/detectors/confluent/v1/confluent.go +++ b/pkg/detectors/confluent/v1/confluent.go @@ -35,7 +35,7 @@ func (s Scanner) Keywords() []string { return []string{"confluent"} } -func (Scanner) Version() int { return 1 } +func (s Scanner) Version() int { return 1 } // FromData will find and optionally verify Confluent secrets in a given set of bytes. func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { diff --git a/pkg/detectors/confluent/v2/confluent.go b/pkg/detectors/confluent/v2/confluent.go index f13f9445abf9..a294cb433dd6 100644 --- a/pkg/detectors/confluent/v2/confluent.go +++ b/pkg/detectors/confluent/v2/confluent.go @@ -21,9 +21,10 @@ type Scanner struct { var _ detectors.Detector = (*Scanner)(nil) var ( + keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"confluent"}) + `\b([a-zA-Z0-9]{16})\b`) // Match cflt prefix followed by 60 characters consisting of A-Z, a-z, 0-9, + or / //See https://docs.confluent.io/cloud/current/security/authenticate/workload-identities/service-accounts/api-keys/overview.html#api-secret-format - secretPat = regexp.MustCompile(detectors.PrefixRegex([]string{"Confluent"}) + `\b(cflt[A-Za-z0-9+/]{60})\b`) + secretPat = regexp.MustCompile(`\b(cflt[A-Za-z0-9+/]{60})\b`) ) // Keywords are used for efficiently pre-filtering chunks. @@ -32,31 +33,37 @@ func (s Scanner) Keywords() []string { return []string{"cflt"} } -func (Scanner) Version() int { return 2 } +func (s Scanner) Version() int { return 2 } // FromData will find and optionally verify Confluent 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) secretMatches := secretPat.FindAllStringSubmatch(dataStr, -1) - for _, match := range secretMatches { - resSecret := strings.TrimSpace(match[1]) // Use index 1 for the captured group + for _, match := range matches { + resMatch := strings.TrimSpace(match[1]) - s1 := detectors.Result{ - DetectorType: detectorspb.DetectorType_Confluent, - Raw: []byte(resSecret), - ExtraData: map[string]string{ - "rotation_guide": "https://docs.confluent.io/cloud/current/security/authenticate/workload-identities/service-accounts/api-keys/best-practices-api-keys.html#rotate-api-keys-regularly", - "version": fmt.Sprintf("%d", s.Version()), - }, - } + for _, match := range secretMatches { + resSecret := strings.TrimSpace(match[1]) // Use index 1 for the captured group - if verify { - s1.Verified = verifyConfluentSecret(resSecret) - } + s1 := detectors.Result{ + DetectorType: detectorspb.DetectorType_Confluent, + Raw: []byte(resMatch), + RawV2: []byte(resMatch + resSecret), + ExtraData: map[string]string{ + "rotation_guide": "https://docs.confluent.io/cloud/current/security/authenticate/workload-identities/service-accounts/api-keys/best-practices-api-keys.html#rotate-api-keys-regularly", + "version": fmt.Sprintf("%d", s.Version()), + }, + } - results = append(results, s1) + if verify { + s1.Verified = verifyConfluentSecret(resSecret) + } + + results = append(results, s1) + } } return results, nil diff --git a/pkg/detectors/confluent/v2/confluent_integration_test.go b/pkg/detectors/confluent/v2/confluent_integration_test.go index 9e5f98eda39f..ddbfb2131641 100644 --- a/pkg/detectors/confluent/v2/confluent_integration_test.go +++ b/pkg/detectors/confluent/v2/confluent_integration_test.go @@ -18,7 +18,8 @@ func TestConfluent_FromChunk(t *testing.T) { // Valid secret with proper CRC32 checksum validSecret := "cfltT8d8RzkNseTMEDKcjNM1BZTFPHqRn/dQm9q7w6SjzZ12wZfwjaJdipHZtDjw" // Invalid secret with wrong checksum - invalidSecret := "cfltT8d8RzkNseTMEDKcjNM1BZTFPHqRn/dQm9q7w6SjzZ12wZfwjaJdipHZtDji" + invalidSecret := "cfltT8d8RzkNseTMEDKcjNM1BZTFPHqRn/dQm9q7w6SjzZ12wZfwjaJdipHZtDje" + key := "JSAOOCIC74SGECCP" type args struct { ctx context.Context @@ -37,7 +38,7 @@ func TestConfluent_FromChunk(t *testing.T) { s: Scanner{}, args: args{ ctx: context.Background(), - data: []byte(fmt.Sprintf("You can find a confluent secret %s", validSecret)), + data: []byte(fmt.Sprintf("You can find a confluent secret %s with key %s", validSecret, key)), verify: true, }, want: []detectors.Result{ @@ -57,7 +58,7 @@ func TestConfluent_FromChunk(t *testing.T) { s: Scanner{}, args: args{ ctx: context.Background(), - data: []byte(fmt.Sprintf("You can find a confluent secret %s but not valid", invalidSecret)), // the secret would satisfy the regex but not pass validation + data: []byte(fmt.Sprintf("You can find a confluent secret %s with %s key but not valid", invalidSecret, key)), // the secret would satisfy the regex but not pass validation verify: true, }, want: []detectors.Result{ diff --git a/pkg/detectors/confluent/v2/confluent_test.go b/pkg/detectors/confluent/v2/confluent_test.go index dc002453ace4..8b4a8a3bc36b 100644 --- a/pkg/detectors/confluent/v2/confluent_test.go +++ b/pkg/detectors/confluent/v2/confluent_test.go @@ -12,24 +12,19 @@ import ( var ( validPattern = ` - # Configuration File: config.yaml - database: - host: $DB_HOST - port: $DB_PORT - username: $DB_USERNAME - password: $DB_PASS # IMPORTANT: Do not share this password publicly + === Confluent Cloud API key === - api: - auth_type: "Basic" - in: "Header" - confluent_secret: "cfltT8d8RzkNseTMEDKcjNM1BZTFPHqRn/dQm9q7w6SjzZ12wZfwjaJdipj0ObHQ" - base_url: "https://api.example.com/v1/user" + API key: + JSAOOCIC74SGECCP + + API secret: + cfltT8d8RzkNseTMEDKcjNM1BZTFPHqRn/dQm9q7w6SjzZ12wZfwjaJdipHZtDjw + + Resource scope: + Cloud resource management - # Notes: - # - Remember to rotate the secret every 90 days. - # - The above credentials should only be used in a secure environment. ` - secret = "cfltT8d8RzkNseTMEDKcjNM1BZTFPHqRn/dQm9q7w6SjzZ12wZfwjaJdipj0ObHQ" + secret = "JSAOOCIC74SGECCPcfltT8d8RzkNseTMEDKcjNM1BZTFPHqRn/dQm9q7w6SjzZ12wZfwjaJdipHZtDjw" ) func TestConfluent_Pattern(t *testing.T) { From 568ea8964a1aaa8d1ddb34eb0ee09886aa11c76e Mon Sep 17 00:00:00 2001 From: Apoorv Munshi Date: Thu, 9 Oct 2025 09:17:02 -0700 Subject: [PATCH 4/4] fix regex for API key in V2 detector --- pkg/detectors/confluent/v2/confluent.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/detectors/confluent/v2/confluent.go b/pkg/detectors/confluent/v2/confluent.go index a294cb433dd6..dcdf998c0025 100644 --- a/pkg/detectors/confluent/v2/confluent.go +++ b/pkg/detectors/confluent/v2/confluent.go @@ -21,7 +21,7 @@ type Scanner struct { var _ detectors.Detector = (*Scanner)(nil) var ( - keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"confluent"}) + `\b([a-zA-Z0-9]{16})\b`) + keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"confluent"}) + `\b([A-Z0-9]{16})\b`) // Match cflt prefix followed by 60 characters consisting of A-Z, a-z, 0-9, + or / //See https://docs.confluent.io/cloud/current/security/authenticate/workload-identities/service-accounts/api-keys/overview.html#api-secret-format secretPat = regexp.MustCompile(`\b(cflt[A-Za-z0-9+/]{60})\b`)