Skip to content

Commit 8d5986d

Browse files
Added v2 for CircleCI Detector (#4300)
* Added v2 for CircleCI Detector * resolved comments * resolved comments
1 parent 55cfe6b commit 8d5986d

File tree

7 files changed

+367
-34
lines changed

7 files changed

+367
-34
lines changed

pkg/detectors/circleci/v1/circleci.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package circleci
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"strconv"
9+
10+
regexp "github.com/wasilibs/go-re2"
11+
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
14+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
15+
)
16+
17+
type Scanner struct {
18+
client *http.Client
19+
}
20+
21+
// Ensure the Scanner satisfies the interface at compile time.
22+
var _ detectors.Detector = (*Scanner)(nil)
23+
var _ detectors.Versioner = (*Scanner)(nil)
24+
25+
var (
26+
defaultClient = common.SaneHttpClient()
27+
28+
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"circle"}) + `([a-fA-F0-9]{40})`)
29+
)
30+
31+
func (s Scanner) getClient() *http.Client {
32+
if s.client != nil {
33+
return s.client
34+
}
35+
36+
return defaultClient
37+
}
38+
39+
func (Scanner) Version() int { return 1 }
40+
41+
// Keywords are used for efficiently pre-filtering chunks.
42+
// Use identifiers in the secret preferably, or the provider name.
43+
func (s Scanner) Keywords() []string {
44+
return []string{"circle"}
45+
}
46+
47+
func (s Scanner) Type() detectorspb.DetectorType {
48+
return detectorspb.DetectorType_Circle
49+
}
50+
51+
func (s Scanner) Description() string {
52+
return "CircleCI is a continuous integration and delivery platform used to build, test, and deploy software. CircleCI tokens can be used to interact with the CircleCI API and access various resources and functionalities."
53+
}
54+
55+
// FromData will find and optionally verify Circle secrets in a given set of bytes.
56+
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
57+
dataStr := string(data)
58+
59+
var uniqueTokens = make(map[string]struct{})
60+
61+
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
62+
uniqueTokens[match[1]] = struct{}{}
63+
}
64+
65+
for token := range uniqueTokens {
66+
result := detectors.Result{
67+
DetectorType: detectorspb.DetectorType_Circle,
68+
Raw: []byte(token),
69+
ExtraData: map[string]string{
70+
"Version": strconv.Itoa(s.Version()),
71+
},
72+
}
73+
74+
if verify {
75+
// https://circleci.com/docs/api/#authentication
76+
isVerified, verificationErr := VerifyCircleCIToken(ctx, s.getClient(), token)
77+
result.Verified = isVerified
78+
result.SetVerificationError(verificationErr, token)
79+
}
80+
81+
results = append(results, result)
82+
}
83+
84+
return
85+
}
86+
87+
func VerifyCircleCIToken(ctx context.Context, client *http.Client, token string) (bool, error) {
88+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://circleci.com/api/v2/me", http.NoBody)
89+
if err != nil {
90+
return false, err
91+
}
92+
93+
req.Header.Add("Accept", "application/json;")
94+
req.Header.Add("Circle-Token", token)
95+
96+
resp, err := client.Do(req)
97+
if err != nil {
98+
return false, err
99+
}
100+
101+
defer func() {
102+
_, _ = io.Copy(io.Discard, resp.Body)
103+
_ = resp.Body.Close()
104+
}()
105+
106+
switch resp.StatusCode {
107+
case http.StatusOK:
108+
return true, nil
109+
case http.StatusUnauthorized:
110+
return false, nil
111+
default:
112+
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
113+
}
114+
}

pkg/detectors/circleci/circleci.go renamed to pkg/detectors/circleci/v2/circleci.go

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,73 +3,81 @@ package circleci
33
import (
44
"context"
55
"net/http"
6+
"strconv"
67

78
regexp "github.com/wasilibs/go-re2"
89

10+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
911
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
12+
v1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/circleci/v1"
1013
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
11-
12-
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
1314
)
1415

15-
type Scanner struct{}
16+
type Scanner struct {
17+
client *http.Client
18+
}
1619

1720
// Ensure the Scanner satisfies the interface at compile time.
1821
var _ detectors.Detector = (*Scanner)(nil)
22+
var _ detectors.Versioner = (*Scanner)(nil)
1923

2024
var (
21-
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"circle"}) + `([a-fA-F0-9]{40})`)
25+
defaultClient = common.SaneHttpClient()
26+
27+
keyPat = regexp.MustCompile(`(CCIPAT_[a-zA-Z0-9]{22}_[a-fA-F0-9]{40})`)
2228
)
2329

30+
func (s Scanner) getClient() *http.Client {
31+
if s.client != nil {
32+
return s.client
33+
}
34+
35+
return defaultClient
36+
}
37+
38+
func (Scanner) Version() int { return 2 }
39+
2440
// Keywords are used for efficiently pre-filtering chunks.
2541
// Use identifiers in the secret preferably, or the provider name.
2642
func (s Scanner) Keywords() []string {
27-
return []string{"circle"}
43+
return []string{"CCIPAT_"}
44+
}
45+
46+
func (s Scanner) Type() detectorspb.DetectorType {
47+
return detectorspb.DetectorType_Circle
48+
}
49+
50+
func (s Scanner) Description() string {
51+
return "CircleCI is a continuous integration and delivery platform used to build, test, and deploy software. CircleCI tokens can be used to interact with the CircleCI API and access various resources and functionalities."
2852
}
2953

3054
// FromData will find and optionally verify Circle secrets in a given set of bytes.
3155
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
3256
dataStr := string(data)
3357

34-
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
58+
var uniqueTokens = make(map[string]struct{})
3559

36-
for _, match := range matches {
37-
38-
token := match[1]
60+
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
61+
uniqueTokens[match[1]] = struct{}{}
62+
}
3963

64+
for token := range uniqueTokens {
4065
result := detectors.Result{
4166
DetectorType: detectorspb.DetectorType_Circle,
4267
Raw: []byte(token),
68+
ExtraData: map[string]string{
69+
"Version": strconv.Itoa(s.Version()),
70+
},
4371
}
4472

4573
if verify {
46-
client := common.SaneHttpClient()
47-
// https://circleci.com/docs/api/#authentication
48-
req, err := http.NewRequestWithContext(ctx, "GET", "https://circleci.com/api/v2/me", nil)
49-
if err != nil {
50-
continue
51-
}
52-
req.Header.Add("Accept", "application/json;")
53-
req.Header.Add("Circle-Token", token)
54-
res, err := client.Do(req)
55-
if err == nil {
56-
defer res.Body.Close()
57-
}
58-
if res != nil && res.StatusCode >= 200 && res.StatusCode < 300 {
59-
result.Verified = true
60-
}
74+
isVerified, verificationErr := v1.VerifyCircleCIToken(ctx, s.getClient(), token)
75+
result.Verified = isVerified
76+
result.SetVerificationError(verificationErr, token)
6177
}
6278

6379
results = append(results, result)
6480
}
6581

6682
return
6783
}
68-
69-
func (s Scanner) Type() detectorspb.DetectorType {
70-
return detectorspb.DetectorType_Circle
71-
}
72-
73-
func (s Scanner) Description() string {
74-
return "CircleCI is a continuous integration and delivery platform used to build, test, and deploy software. CircleCI tokens can be used to interact with the CircleCI API and access various resources and functionalities."
75-
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//go:build detectors
2+
// +build detectors
3+
4+
package circleci
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"os"
10+
"testing"
11+
"time"
12+
13+
"github.com/kylelemons/godebug/pretty"
14+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
15+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
16+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
17+
)
18+
19+
func TestCircleCI_FromChunk(t *testing.T) {
20+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
21+
defer cancel()
22+
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
23+
if err != nil {
24+
t.Fatalf("could not get test secrets from GCP: %s", err)
25+
}
26+
secret := testSecrets.MustGetField("CIRCLECI")
27+
secretInactive := testSecrets.MustGetField("CIRCLECI_INACTIVE")
28+
type args struct {
29+
ctx context.Context
30+
data []byte
31+
verify bool
32+
}
33+
tests := []struct {
34+
name string
35+
s Scanner
36+
args args
37+
want []detectors.Result
38+
wantErr bool
39+
}{
40+
{
41+
name: "found, verified",
42+
s: Scanner{},
43+
args: args{
44+
ctx: context.Background(),
45+
data: []byte(fmt.Sprintf("You can find a circle secret %s within", secret)),
46+
verify: true,
47+
},
48+
want: []detectors.Result{
49+
{
50+
DetectorType: detectorspb.DetectorType_Circle,
51+
Verified: true,
52+
},
53+
},
54+
wantErr: false,
55+
},
56+
{
57+
name: "found, unverified",
58+
s: Scanner{},
59+
args: args{
60+
ctx: context.Background(),
61+
data: []byte(fmt.Sprintf("You can find a circle secret %s within", secretInactive)),
62+
verify: true,
63+
},
64+
want: []detectors.Result{
65+
{
66+
DetectorType: detectorspb.DetectorType_Circle,
67+
Verified: false,
68+
},
69+
},
70+
wantErr: false,
71+
},
72+
{
73+
name: "not found",
74+
s: Scanner{},
75+
args: args{
76+
ctx: context.Background(),
77+
data: []byte("You cannot find the secret within"),
78+
verify: true,
79+
},
80+
want: nil,
81+
wantErr: false,
82+
},
83+
}
84+
for _, tt := range tests {
85+
t.Run(tt.name, func(t *testing.T) {
86+
s := Scanner{}
87+
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
88+
if (err != nil) != tt.wantErr {
89+
t.Errorf("CircleCI.FromData() error = %v, wantErr %v", err, tt.wantErr)
90+
return
91+
}
92+
if os.Getenv("FORCE_PASS_DIFF") == "true" {
93+
return
94+
}
95+
for i := range got {
96+
if len(got[i].Raw) == 0 {
97+
t.Fatal("no raw secret present")
98+
}
99+
got[i].Raw = nil
100+
got[i].ExtraData = nil
101+
}
102+
if diff := pretty.Compare(got, tt.want); diff != "" {
103+
t.Errorf("CircleCI.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
104+
}
105+
})
106+
}
107+
}
108+
109+
func BenchmarkFromData(benchmark *testing.B) {
110+
ctx := context.Background()
111+
s := Scanner{}
112+
for name, data := range detectors.MustGetBenchmarkData() {
113+
benchmark.Run(name, func(b *testing.B) {
114+
b.ResetTimer()
115+
for n := 0; n < b.N; n++ {
116+
_, err := s.FromData(ctx, false, data)
117+
if err != nil {
118+
b.Fatal(err)
119+
}
120+
}
121+
})
122+
}
123+
}

0 commit comments

Comments
 (0)