Skip to content

Commit 76c63fe

Browse files
Fixed and improved Clickhelp detector (#4384)
1 parent 626887c commit 76c63fe

File tree

3 files changed

+105
-70
lines changed

3 files changed

+105
-70
lines changed

pkg/detectors/clickhelp/clickhelp.go

Lines changed: 59 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ package clickhelp
22

33
import (
44
"context"
5-
b64 "encoding/base64"
65
"fmt"
6+
"io"
77
"net/http"
8-
"strings"
98

109
regexp "github.com/wasilibs/go-re2"
1110

@@ -25,55 +24,57 @@ var (
2524
client = common.SaneHttpClient()
2625

2726
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
28-
serverPat = regexp.MustCompile(`\b([0-9A-Za-z]{3,20}.try.clickhelp.co)\b`)
29-
emailPat = regexp.MustCompile(`\b([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-z]+)\b`)
30-
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"clickhelp"}) + `\b([0-9A-Za-z]{24})\b`)
27+
portalPat = regexp.MustCompile(`\b([0-9A-Za-z-]{3,20}\.(?:try\.)?clickhelp\.co)\b`)
28+
emailPat = regexp.MustCompile(common.EmailPattern)
29+
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"clickhelp", "key", "token", "api", "secret"}) + `\b([0-9A-Za-z]{24})\b`)
3130
)
3231

32+
func (s Scanner) Type() detectorspb.DetectorType {
33+
return detectorspb.DetectorType_ClickHelp
34+
}
35+
36+
func (s Scanner) Description() string {
37+
return "ClickHelp is a documentation tool that allows users to create and manage online documentation. ClickHelp API keys can be used to access and modify documentation data."
38+
}
39+
3340
// Keywords are used for efficiently pre-filtering chunks.
3441
// Use identifiers in the secret preferably, or the provider name.
3542
func (s Scanner) Keywords() []string {
36-
return []string{"clickhelp"}
43+
return []string{"clickhelp.co"}
3744
}
3845

3946
// FromData will find and optionally verify Clickhelp secrets in a given set of bytes.
4047
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
4148
dataStr := string(data)
4249

43-
serverMatches := serverPat.FindAllStringSubmatch(dataStr, -1)
44-
emailMatches := emailPat.FindAllStringSubmatch(dataStr, -1)
45-
keyMatches := keyPat.FindAllStringSubmatch(dataStr, -1)
50+
var uniquePortalLinks, uniqueEmails, uniqueAPIKeys = make(map[string]struct{}), make(map[string]struct{}), make(map[string]struct{})
4651

47-
for _, match := range serverMatches {
48-
resServer := strings.TrimSpace(match[1])
52+
for _, match := range portalPat.FindAllStringSubmatch(dataStr, -1) {
53+
uniquePortalLinks[match[1]] = struct{}{}
54+
}
55+
56+
for _, match := range emailPat.FindAllStringSubmatch(dataStr, -1) {
57+
uniqueEmails[match[1]] = struct{}{}
58+
}
4959

50-
for _, emailMatch := range emailMatches {
51-
resEmail := strings.TrimSpace(emailMatch[1])
52-
for _, keyMatch := range keyMatches {
53-
resKey := strings.TrimSpace(keyMatch[1])
60+
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
61+
uniqueAPIKeys[match[1]] = struct{}{}
62+
}
5463

64+
for portalLink := range uniquePortalLinks {
65+
for email := range uniqueEmails {
66+
for apiKey := range uniqueAPIKeys {
5567
s1 := detectors.Result{
5668
DetectorType: detectorspb.DetectorType_ClickHelp,
57-
Raw: []byte(resServer),
58-
RawV2: []byte(resServer + resEmail),
69+
Raw: []byte(portalLink),
70+
RawV2: []byte(portalLink + email),
5971
}
6072

6173
if verify {
62-
data := fmt.Sprintf("%s:%s", resEmail, resKey)
63-
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
64-
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://%s/api/v1/projects", resServer), nil)
65-
if err != nil {
66-
continue
67-
}
68-
req.Header.Add("Content-Type", "application/json")
69-
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc))
70-
res, err := client.Do(req)
71-
if err == nil {
72-
defer res.Body.Close()
73-
if res.StatusCode >= 200 && res.StatusCode < 300 {
74-
s1.Verified = true
75-
}
76-
}
74+
isVerified, verificationErr := verifyClickHelp(ctx, client, portalLink, email, apiKey)
75+
s1.Verified = isVerified
76+
s1.SetVerificationError(verificationErr)
77+
s1.SetPrimarySecretValue(apiKey) // line number will point to api key
7778
}
7879

7980
results = append(results, s1)
@@ -84,10 +85,31 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
8485
return results, nil
8586
}
8687

87-
func (s Scanner) Type() detectorspb.DetectorType {
88-
return detectorspb.DetectorType_ClickHelp
89-
}
88+
func verifyClickHelp(ctx context.Context, client *http.Client, portalLink, email, apiKey string) (bool, error) {
89+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s/api/v1/projects", portalLink), http.NoBody)
90+
if err != nil {
91+
return false, err
92+
}
9093

91-
func (s Scanner) Description() string {
92-
return "ClickHelp is a documentation tool that allows users to create and manage online documentation. ClickHelp API keys can be used to access and modify documentation data."
94+
req.Header.Add("Content-Type", "application/json")
95+
req.SetBasicAuth(email, apiKey)
96+
97+
resp, err := client.Do(req)
98+
if err != nil {
99+
return false, err
100+
}
101+
102+
defer func() {
103+
_, _ = io.Copy(io.Discard, resp.Body)
104+
_ = resp.Body.Close()
105+
}()
106+
107+
switch resp.StatusCode {
108+
case http.StatusOK:
109+
return true, nil
110+
case http.StatusUnauthorized:
111+
return false, nil
112+
default:
113+
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
114+
}
93115
}

pkg/detectors/clickhelp/clickhelp_integration_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import (
99
"testing"
1010
"time"
1111

12-
"github.com/kylelemons/godebug/pretty"
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/google/go-cmp/cmp/cmpopts"
1314
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
1415

1516
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
@@ -97,9 +98,11 @@ func TestClickhelp_FromChunk(t *testing.T) {
9798
t.Fatalf("no raw secret present: \n %+v", got[i])
9899
}
99100
got[i].Raw = nil
101+
got[i].RawV2 = nil
100102
}
101-
if diff := pretty.Compare(got, tt.want); diff != "" {
102-
t.Errorf("Clickhelp.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
103+
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "primarySecret")
104+
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
105+
t.Errorf("AdafruitIO.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
103106
}
104107
})
105108
}

pkg/detectors/clickhelp/clickhelp_test.go

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,6 @@ import (
1010
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
1111
)
1212

13-
var (
14-
validPattern = `
15-
# Configuration File: config.yaml
16-
database:
17-
host: $DB_HOST
18-
port: $DB_PORT
19-
username: $DB_USERNAME
20-
password: $DB_PASS # IMPORTANT: Do not share this password publicly
21-
22-
api:
23-
user_email: "[email protected]"
24-
clickhelp_key: "XzUkp562BtmjfRGoOGBiLLNu"
25-
clickhelp_domain: testingdev.try.clickhelp.co
26-
auth_type: Basic
27-
base_url: "https://testing-dev.try.clickhelp.co/v1/user"
28-
auth_token: ""
29-
30-
# Notes:
31-
# - Remember to rotate the secret every 90 days.
32-
# - The above credentials should only be used in a secure environment.
33-
`
34-
secrets = []string{
35-
36-
37-
}
38-
)
39-
4013
func TestClickHelp_Pattern(t *testing.T) {
4114
d := Scanner{}
4215
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
@@ -47,9 +20,46 @@ func TestClickHelp_Pattern(t *testing.T) {
4720
want []string
4821
}{
4922
{
50-
name: "valid pattern",
51-
input: validPattern,
52-
want: secrets,
23+
name: "valid pattern",
24+
input: `
25+
# Configuration File: config.yaml
26+
database:
27+
host: $DB_HOST
28+
port: $DB_PORT
29+
username: $DB_USERNAME
30+
password: $DB_PASS # IMPORTANT: Do not share this password publicly
31+
32+
api:
33+
user_email: "[email protected]"
34+
key: "XzUkp562BtmjfRGoOGBiLLNu"
35+
portal: testingdev.try.clickhelp.co
36+
auth_type: Basic
37+
base_url: "https://testing-dev.try.clickhelp.co/v1/user"
38+
auth_token: ""
39+
40+
# Notes:
41+
# - Remember to rotate the secret every 90 days.
42+
# - The above credentials should only be used in a secure environment.
43+
`,
44+
want: []string{
45+
46+
47+
},
48+
},
49+
{
50+
name: "valid pattern - xml",
51+
input: `
52+
<com.cloudbees.plugins.credentials.impl.StringCredentialsImpl>
53+
<scope>GLOBAL</scope>
54+
<id>{[email protected]}</id>
55+
<secret>{AQAAABAAA XzUkp562BtmjfRGoOGBiLLNu}</secret>
56+
<portal>company-prod.clickhelp.co</portal>
57+
<description>configuration for production</description>
58+
<creationDate>2023-05-18T14:32:10Z</creationDate>
59+
<owner>jenkins-admin</owner>
60+
</com.cloudbees.plugins.credentials.impl.StringCredentialsImpl>
61+
`,
62+
want: []string{"[email protected]"},
5363
},
5464
}
5565

0 commit comments

Comments
 (0)