Skip to content
Merged
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
139 changes: 139 additions & 0 deletions pkg/detectors/clientary/clientary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
RoninApp rebranded to Clientary

Article: https://www.clientary.com/articles/a-new-brand/
*/
package clientary

import (
"context"
"errors"
"fmt"
"io"
"net/http"

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
}

// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)

var (
client = common.SaneHttpClient()

// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ronin", "clientary"}) + `\b([0-9a-zA-Z]{24,26})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ronin", "clientary"}) + `\b([0-9Aa-zA-Z-]{4,25})\b`)

errAccountNotFound = errors.New("account not found")
)

// 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{"ronin", "clientary"}
}

func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Clientary
}

func (s Scanner) Description() string {
return "Clientary is a one software app to manage Clients, Invoices, Projects, Proposals, Estimates, Hours, Payments, Contractors and Staff. Clientary keys can be used to access and manage invoices and other resources."
}

// FromData will find and optionally verify RoninApp 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)

var uniqueIDs, uniqueAPIKeys = make(map[string]struct{}), make(map[string]struct{})

for _, match := range idPat.FindAllStringSubmatch(dataStr, -1) {
uniqueIDs[match[1]] = struct{}{}
}

for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueAPIKeys[match[1]] = struct{}{}
}

for apiKey := range uniqueAPIKeys {
for id := range uniqueIDs {
// since regex matches can overlap, continue only if both apiKey and id are the same.
if apiKey == id {
continue
}

s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_Clientary,
Raw: []byte(apiKey),
RawV2: []byte(apiKey + ":" + id),
ExtraData: make(map[string]string),
}

if verify {
isVerified, verificationErr := verifyClientaryAPIKey(ctx, client, id, apiKey)
s1.Verified = isVerified
if verificationErr != nil {
// remove the account ID if not found to prevent reuse during other API key checks.
if errors.Is(verificationErr, errAccountNotFound) {
delete(uniqueIDs, id)
continue
}

s1.SetVerificationError(verificationErr, apiKey)
}

// If a verified result is found, attach rebranding documentation to inform the user about the RoninApp rebranding to Clientary.
if s1.Verified {
s1.ExtraData["Rebrading Docs"] = "https://www.clientary.com/articles/a-new-brand/"
}
}

results = append(results, s1)
}

}

return results, nil
}

// docs: https://www.clientary.com/api
func verifyClientaryAPIKey(ctx context.Context, client *http.Client, id, apiKey string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://"+id+".clientary.com/api/v2/invoices", http.NoBody)
if err != nil {
return false, nil
}

req.SetBasicAuth(apiKey, apiKey)
req.Header.Add("Accept", "application/json")

resp, err := client.Do(req)
if err != nil {
return false, err
}

defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()

switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusForbidden, http.StatusUnauthorized:
return false, nil
case http.StatusNotFound:
// API return 404 if the account id does not exist
return false, errAccountNotFound
default:
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//go:build detectors
// +build detectors

package roninapp
package clientary

import (
"context"
Expand Down Expand Up @@ -44,13 +44,20 @@ func TestRoninApp_FromChunk(t *testing.T) {
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a roninapp secret %s within roninappdomain %s", secret, domain)),
data: []byte(fmt.Sprintf("You can find a clientary secret %s and clientaryDomain %s", secret, domain)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_RoninApp,
DetectorType: detectorspb.DetectorType_Clientary,
Verified: false,
},
{
DetectorType: detectorspb.DetectorType_Clientary,
Verified: true,
ExtraData: map[string]string{
"Rebrading Docs": "https://www.clientary.com/articles/a-new-brand/",
},
},
},
wantErr: false,
Expand All @@ -60,12 +67,12 @@ func TestRoninApp_FromChunk(t *testing.T) {
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a roninapp secret %s within roninappdomain %s but not valid", inactiveSecret, domain)), // the secret would satisfy the regex but not pass validation
data: []byte(fmt.Sprintf("You can find a ronin secret %s and ronaindomain %s but not valid", inactiveSecret, domain)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_RoninApp,
DetectorType: detectorspb.DetectorType_Clientary,
Verified: false,
},
},
Expand Down Expand Up @@ -96,6 +103,7 @@ func TestRoninApp_FromChunk(t *testing.T) {
t.Fatalf("no raw secret present: \n %+v", got[i])
}
got[i].Raw = nil
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("RoninApp.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package roninapp
package clientary

import (
"context"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
Expand All @@ -11,14 +10,6 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)

var (
validKey = "tmvSxOOxP32WgoPfF2tWEmtsq8"
invalidKey = "tmvSxOOxP32Wg?PfF2tWEmtsq8"
validId = "XEZOhulA1oP7vPSc4K"
invalidId = "?EZOhulA1oP7vPSc4?"
keyword = "roninapp"
)

func TestRoninApp_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
Expand All @@ -28,14 +19,34 @@ func TestRoninApp_Pattern(t *testing.T) {
want []string
}{
{
name: "valid pattern - with keyword roninapp",
input: fmt.Sprintf("%s token - '%s'\n%s token - '%s'\n", keyword, validKey, keyword, validId),
want: []string{validKey, validKey},
name: "valid pattern - with keyword ronin",
input: `
# some random code
data := getIDFromDatabase(ctx)
roninAPIKey := ZycQ0G6IBgNsBWytwzwVKixyz
roninDomain := truffle-dev.roninapp.com
`,
want: []string{"ZycQ0G6IBgNsBWytwzwVKixyz:truffle-dev"},
},
{
name: "valid pattern - with keyword clientary",
input: `
# some random code
data := getIDFromDatabase(ctx)
clientaryAPIKey := ZycQ0G6IBgNsBWytwzwVKixyz
clientaryDomain := truffle-dev.clientary.com
`,
want: []string{"ZycQ0G6IBgNsBWytwzwVKixyz:truffle-dev"},
},
{
name: "invalid pattern",
input: fmt.Sprintf("%s token - '%s'\n%s token - '%s'\n", keyword, invalidKey, keyword, invalidId),
want: []string{},
name: "invalid pattern",
input: `
# some random code
data := getIDFromDatabase(ctx)
roninAPIKey := ZycQ0G6IBg-NsBWytwzwVKixyz
rominDomain := t_de.roninapp.com
`,
want: []string{},
},
}

Expand Down
87 changes: 0 additions & 87 deletions pkg/detectors/roninapp/roninapp.go

This file was deleted.

4 changes: 2 additions & 2 deletions pkg/engine/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/clicksendsms"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/clickuppersonaltoken"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/cliengo"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/clientary"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/clinchpad"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/clockify"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/clockworksms"
Expand Down Expand Up @@ -611,7 +612,6 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/roaring"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/robinhoodcrypto"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/rocketreach"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/roninapp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/route4me"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/rownd"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/rubygems"
Expand Down Expand Up @@ -1003,6 +1003,7 @@ func buildDetectorList() []detectors.Detector {
&clicksendsms.Scanner{},
&clickuppersonaltoken.Scanner{},
&cliengo.Scanner{},
&clientary.Scanner{},
&clinchpad.Scanner{},
&clockify.Scanner{},
&clockworksms.Scanner{},
Expand Down Expand Up @@ -1478,7 +1479,6 @@ func buildDetectorList() []detectors.Detector {
&robinhoodcrypto.Scanner{},
&rocketreach.Scanner{},
// &rockset.Scanner{},
&roninapp.Scanner{},
&route4me.Scanner{},
&rownd.Scanner{},
&rubygems.Scanner{},
Expand Down
Loading