Skip to content

Commit c3f0e1c

Browse files
Updated LaunchDarkly Analyzer and Detector (#4178)
* updated launchdarkly analyzer and detector * updated launchdarkly analyzer test * fixed linter * remove extradata nil check
1 parent 92e9157 commit c3f0e1c

File tree

6 files changed

+115
-64
lines changed

6 files changed

+115
-64
lines changed

pkg/analyzer/analyzers/launchdarkly/launchdarkly.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"os"
8+
"strings"
89

910
"github.com/fatih/color"
1011
"github.com/jedib0t/go-pretty/v6/table"
@@ -31,6 +32,10 @@ func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analy
3132
return nil, errors.New("key not found in credentials info")
3233
}
3334

35+
if isSDKKey(key) {
36+
return nil, errors.New("sdk keys cannot be analyzed")
37+
}
38+
3439
info, err := AnalyzePermissions(a.Cfg, key)
3540
if err != nil {
3641
return nil, err
@@ -40,6 +45,13 @@ func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analy
4045
}
4146

4247
func AnalyzeAndPrintPermissions(cfg *config.Config, token string) {
48+
if isSDKKey(token) {
49+
color.Yellow("\n[!] The Provided key is an SDK Key. SDK Keys are sensitive but used to configure LaunchDarkly SDKs")
50+
color.Green("\n[i] Docs: https://launchdarkly.com/docs/home/account/environment/settings#copy-and-reset-sdk-credentials-for-an-environment")
51+
52+
return
53+
}
54+
4355
info, err := AnalyzePermissions(cfg, token)
4456
if err != nil {
4557
// just print the error in cli and continue as a partial success
@@ -86,7 +98,7 @@ func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
8698
}
8799

88100
result := analyzers.AnalyzerResult{
89-
AnalyzerType: analyzers.AnalyzerTypeElevenLabs,
101+
AnalyzerType: analyzers.AnalyzerTypeLaunchDarkly,
90102
Metadata: map[string]any{},
91103
Bindings: make([]analyzers.Binding, 0),
92104
}
@@ -202,3 +214,8 @@ func printResources(resources []Resource) {
202214
}
203215
callerTable.Render()
204216
}
217+
218+
// isSDKKey check if the key provided is an SDK Key or not
219+
func isSDKKey(key string) bool {
220+
return strings.HasPrefix(key, "sdk-")
221+
}

pkg/analyzer/analyzers/launchdarkly/launchdarkly_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestAnalyzer_Analyze(t *testing.T) {
3333
wantErr bool
3434
}{
3535
{
36-
name: "valid LauncgDarkly token",
36+
name: "valid LaunchDarkly token",
3737
key: key,
3838
want: expectedOutput,
3939
wantErr: false,

pkg/analyzer/analyzers/launchdarkly/result_output.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"AnalyzerType": 6,
2+
"AnalyzerType": 31,
33
"Bindings": [
44
{
55
"Resource": {
@@ -29,7 +29,10 @@
2929
"Name": "Roxanne Tampus",
3030
"FullyQualifiedName": "launchdarkly/member/61543c5956be60235562486f",
3131
"Type": "Member",
32-
"Metadata": {},
32+
"Metadata": {
33+
"Email": "[email protected]",
34+
"Role": "owner"
35+
},
3336
"Parent": null
3437
},
3538
"Permission": {
@@ -80,7 +83,9 @@
8083
"Name": "secretscanner",
8184
"FullyQualifiedName": "launchdarkly/proj/default/flag/secretscanner",
8285
"Type": "Feature Flags",
83-
"Metadata": {},
86+
"Metadata": {
87+
"Kind": "boolean"
88+
},
8489
"Parent": {
8590
"Name": "secretscanner",
8691
"FullyQualifiedName": "launchdarkly/proj/61543c5956be60235562486e",

pkg/detectors/launchdarkly/launchdarkly.go

Lines changed: 72 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"net/http"
89
"strings"
910

@@ -44,6 +45,14 @@ type callerIdentity struct {
4445
ServiceToken bool `json:"serviceToken"`
4546
}
4647

48+
func (s Scanner) getClient() *http.Client {
49+
if s.client == nil {
50+
return defaultClient
51+
}
52+
53+
return s.client
54+
}
55+
4756
// We are not including "mob-" because client keys are not sensitive.
4857
// They are expected to be public.
4958
func (s Scanner) Keywords() []string {
@@ -66,54 +75,16 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
6675
}
6776

6877
if verify {
69-
req, err := http.NewRequestWithContext(ctx, "GET", "https://app.launchdarkly.com/api/v2/caller-identity", nil)
70-
if err != nil {
71-
continue
72-
}
73-
client := s.client
74-
if client == nil {
75-
client = defaultClient
76-
}
77-
req.Header.Add("Authorization", resMatch)
78-
res, err := client.Do(req)
79-
if err == nil {
80-
defer res.Body.Close()
81-
if res.StatusCode >= 200 && res.StatusCode < 300 {
82-
s1.Verified = true
83-
var callerIdentity callerIdentity
84-
if err := json.NewDecoder(res.Body).Decode(&callerIdentity); err == nil { // no error in parsing
85-
s1.ExtraData["type"] = callerIdentity.TokenKind
86-
s1.ExtraData["account_id"] = callerIdentity.AccountId
87-
s1.ExtraData["environment_id"] = callerIdentity.EnvironmentId
88-
s1.ExtraData["project_id"] = callerIdentity.ProjectId
89-
s1.ExtraData["environment_name"] = callerIdentity.EnvironmentName
90-
s1.ExtraData["project_name"] = callerIdentity.ProjectName
91-
s1.ExtraData["auth_kind"] = callerIdentity.AuthKind
92-
s1.ExtraData["token_kind"] = callerIdentity.TokenKind
93-
s1.ExtraData["client_id"] = callerIdentity.ClientID
94-
s1.ExtraData["token_name"] = callerIdentity.TokenName
95-
s1.ExtraData["member_id"] = callerIdentity.MemberId
96-
if callerIdentity.TokenKind == "auth" {
97-
if callerIdentity.ServiceToken {
98-
s1.ExtraData["token_type"] = "service"
99-
} else {
100-
s1.ExtraData["token_type"] = "personal"
101-
}
102-
}
103-
}
104-
105-
s1.AnalysisInfo = map[string]string{
106-
"key": resMatch,
107-
}
108-
109-
} else if res.StatusCode == 401 {
110-
// 401 is expected for an invalid token, so there is nothing to do here.
111-
} else {
112-
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
113-
s1.SetVerificationError(err, resMatch)
78+
extraData, isVerified, verificationErr := verifyLaunchDarklyKey(ctx, s.getClient(), resMatch)
79+
s1.Verified = isVerified
80+
s1.SetVerificationError(verificationErr)
81+
s1.ExtraData = extraData
82+
83+
// only api keys can be analyzed
84+
if strings.HasPrefix(resMatch, "api-") {
85+
s1.AnalysisInfo = map[string]string{
86+
"key": resMatch,
11487
}
115-
} else {
116-
s1.SetVerificationError(err, resMatch)
11788
}
11889
}
11990

@@ -130,3 +101,57 @@ func (s Scanner) Type() detectorspb.DetectorType {
130101
func (s Scanner) Description() string {
131102
return "LaunchDarkly is a feature management platform that allows teams to control the visibility of features to users. LaunchDarkly API keys can be used to access and modify feature flags and other resources within a LaunchDarkly account."
132103
}
104+
105+
func verifyLaunchDarklyKey(ctx context.Context, client *http.Client, key string) (map[string]string, bool, error) {
106+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://app.launchdarkly.com/api/v2/caller-identity", http.NoBody)
107+
if err != nil {
108+
return nil, false, err
109+
}
110+
111+
req.Header.Add("Authorization", key)
112+
113+
resp, err := client.Do(req)
114+
if err != nil {
115+
return nil, false, err
116+
}
117+
118+
defer func() {
119+
_, _ = io.Copy(io.Discard, resp.Body)
120+
_ = resp.Body.Close()
121+
}()
122+
123+
switch resp.StatusCode {
124+
case http.StatusOK:
125+
var callerIdentity callerIdentity
126+
var extraData = make(map[string]string)
127+
128+
if err := json.NewDecoder(resp.Body).Decode(&callerIdentity); err != nil {
129+
return nil, false, err
130+
}
131+
132+
extraData["type"] = callerIdentity.AccountId
133+
extraData["account"] = callerIdentity.AccountId
134+
extraData["environment_id"] = callerIdentity.EnvironmentId
135+
extraData["project_id"] = callerIdentity.ProjectId
136+
extraData["environment_name"] = callerIdentity.EnvironmentName
137+
extraData["project_name"] = callerIdentity.ProjectName
138+
extraData["auth_kind"] = callerIdentity.AuthKind
139+
extraData["token_kind"] = callerIdentity.TokenKind
140+
extraData["client_id"] = callerIdentity.ClientID
141+
extraData["token_name"] = callerIdentity.TokenName
142+
extraData["member_id"] = callerIdentity.MemberId
143+
if callerIdentity.TokenKind == "auth" {
144+
if callerIdentity.ServiceToken {
145+
extraData["token_type"] = "service"
146+
} else {
147+
extraData["token_type"] = "personal"
148+
}
149+
}
150+
151+
return extraData, true, nil
152+
case http.StatusUnauthorized:
153+
return nil, false, nil
154+
default:
155+
return nil, false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
156+
}
157+
}

pkg/detectors/launchdarkly/launchdarkly_integration_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ func TestLaunchDarkly_FromChunk(t *testing.T) {
129129
t.Fatalf("no raw secret present: \n %+v", got[i])
130130
}
131131
got[i].Raw = nil
132+
got[i].AnalysisInfo = nil
132133

133134
// Do we expect to be comparing the ExtraData?
134135
got[i].ExtraData = nil

pkg/detectors/launchdarkly/launchdarkly_test.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package launchdarkly
22

33
import (
44
"context"
5-
"fmt"
65
"testing"
76

87
"github.com/google/go-cmp/cmp"
@@ -11,12 +10,6 @@ import (
1110
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
1211
)
1312

14-
var (
15-
validPattern = "sdk-5gykyl1b-wgrq-41lc-z6q1-h7zn5fdlcybw"
16-
invalidPattern = "sdk-5gykyl1b-wgrq-41lc-z6q1-h7zn5fdlcyb"
17-
keyword = "launchdarkly"
18-
)
19-
2013
func TestLaunchDarkly_Pattern(t *testing.T) {
2114
d := Scanner{}
2215
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
@@ -26,13 +19,23 @@ func TestLaunchDarkly_Pattern(t *testing.T) {
2619
want []string
2720
}{
2821
{
29-
name: "valid pattern - with keyword launchdarkly",
30-
input: fmt.Sprintf("%s token = '%s'", keyword, validPattern),
31-
want: []string{validPattern},
22+
name: "valid pattern - launchdarkly api key",
23+
input: `api-5gykyl1b-wgrq-41lc-z6q1-h7zn5fdlcybw`,
24+
want: []string{"api-5gykyl1b-wgrq-41lc-z6q1-h7zn5fdlcybw"},
25+
},
26+
{
27+
name: "valid pattern - launchdarkly sdk key",
28+
input: `sdk-5gykyl1b-wgrq-41lc-z6q1-h7zn5fdlcybw`,
29+
want: []string{"sdk-5gykyl1b-wgrq-41lc-z6q1-h7zn5fdlcybw"},
30+
},
31+
{
32+
name: "invalid pattern - invalid length",
33+
input: `sdk-5gykyl1b-wgrq-41lc-z6q1-h7zn5fdlcyb`,
34+
want: []string{},
3235
},
3336
{
34-
name: "invalid pattern",
35-
input: fmt.Sprintf("%s = '%s'", keyword, invalidPattern),
37+
name: "invalid pattern - invalid character",
38+
input: `api-5Gykyl1b-Wgrq-41lC-z6q1-h7zn5fdlcybw`,
3639
want: []string{},
3740
},
3841
}

0 commit comments

Comments
 (0)