Updated LaunchDarkly Analyzer and Detector (#4178)
* updated launchdarkly analyzer and detector * updated launchdarkly analyzer test * fixed linter * remove extradata nil check
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
@@ -31,6 +32,10 @@ func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analy
|
||||
return nil, errors.New("key not found in credentials info")
|
||||
}
|
||||
|
||||
if isSDKKey(key) {
|
||||
return nil, errors.New("sdk keys cannot be analyzed")
|
||||
}
|
||||
|
||||
info, err := AnalyzePermissions(a.Cfg, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -40,6 +45,13 @@ func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analy
|
||||
}
|
||||
|
||||
func AnalyzeAndPrintPermissions(cfg *config.Config, token string) {
|
||||
if isSDKKey(token) {
|
||||
color.Yellow("\n[!] The Provided key is an SDK Key. SDK Keys are sensitive but used to configure LaunchDarkly SDKs")
|
||||
color.Green("\n[i] Docs: https://launchdarkly.com/docs/home/account/environment/settings#copy-and-reset-sdk-credentials-for-an-environment")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
info, err := AnalyzePermissions(cfg, token)
|
||||
if err != nil {
|
||||
// just print the error in cli and continue as a partial success
|
||||
@@ -86,7 +98,7 @@ func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
|
||||
}
|
||||
|
||||
result := analyzers.AnalyzerResult{
|
||||
AnalyzerType: analyzers.AnalyzerTypeElevenLabs,
|
||||
AnalyzerType: analyzers.AnalyzerTypeLaunchDarkly,
|
||||
Metadata: map[string]any{},
|
||||
Bindings: make([]analyzers.Binding, 0),
|
||||
}
|
||||
@@ -202,3 +214,8 @@ func printResources(resources []Resource) {
|
||||
}
|
||||
callerTable.Render()
|
||||
}
|
||||
|
||||
// isSDKKey check if the key provided is an SDK Key or not
|
||||
func isSDKKey(key string) bool {
|
||||
return strings.HasPrefix(key, "sdk-")
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestAnalyzer_Analyze(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid LauncgDarkly token",
|
||||
name: "valid LaunchDarkly token",
|
||||
key: key,
|
||||
want: expectedOutput,
|
||||
wantErr: false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"AnalyzerType": 6,
|
||||
"AnalyzerType": 31,
|
||||
"Bindings": [
|
||||
{
|
||||
"Resource": {
|
||||
@@ -29,7 +29,10 @@
|
||||
"Name": "Roxanne Tampus",
|
||||
"FullyQualifiedName": "launchdarkly/member/61543c5956be60235562486f",
|
||||
"Type": "Member",
|
||||
"Metadata": {},
|
||||
"Metadata": {
|
||||
"Email": "[email protected]",
|
||||
"Role": "owner"
|
||||
},
|
||||
"Parent": null
|
||||
},
|
||||
"Permission": {
|
||||
@@ -80,7 +83,9 @@
|
||||
"Name": "secretscanner",
|
||||
"FullyQualifiedName": "launchdarkly/proj/default/flag/secretscanner",
|
||||
"Type": "Feature Flags",
|
||||
"Metadata": {},
|
||||
"Metadata": {
|
||||
"Kind": "boolean"
|
||||
},
|
||||
"Parent": {
|
||||
"Name": "secretscanner",
|
||||
"FullyQualifiedName": "launchdarkly/proj/61543c5956be60235562486e",
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -44,6 +45,14 @@ type callerIdentity struct {
|
||||
ServiceToken bool `json:"serviceToken"`
|
||||
}
|
||||
|
||||
func (s Scanner) getClient() *http.Client {
|
||||
if s.client == nil {
|
||||
return defaultClient
|
||||
}
|
||||
|
||||
return s.client
|
||||
}
|
||||
|
||||
// We are not including "mob-" because client keys are not sensitive.
|
||||
// They are expected to be public.
|
||||
func (s Scanner) Keywords() []string {
|
||||
@@ -66,54 +75,16 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
}
|
||||
|
||||
if verify {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://app.launchdarkly.com/api/v2/caller-identity", nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
client := s.client
|
||||
if client == nil {
|
||||
client = defaultClient
|
||||
}
|
||||
req.Header.Add("Authorization", resMatch)
|
||||
res, err := client.Do(req)
|
||||
if err == nil {
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||
s1.Verified = true
|
||||
var callerIdentity callerIdentity
|
||||
if err := json.NewDecoder(res.Body).Decode(&callerIdentity); err == nil { // no error in parsing
|
||||
s1.ExtraData["type"] = callerIdentity.TokenKind
|
||||
s1.ExtraData["account_id"] = callerIdentity.AccountId
|
||||
s1.ExtraData["environment_id"] = callerIdentity.EnvironmentId
|
||||
s1.ExtraData["project_id"] = callerIdentity.ProjectId
|
||||
s1.ExtraData["environment_name"] = callerIdentity.EnvironmentName
|
||||
s1.ExtraData["project_name"] = callerIdentity.ProjectName
|
||||
s1.ExtraData["auth_kind"] = callerIdentity.AuthKind
|
||||
s1.ExtraData["token_kind"] = callerIdentity.TokenKind
|
||||
s1.ExtraData["client_id"] = callerIdentity.ClientID
|
||||
s1.ExtraData["token_name"] = callerIdentity.TokenName
|
||||
s1.ExtraData["member_id"] = callerIdentity.MemberId
|
||||
if callerIdentity.TokenKind == "auth" {
|
||||
if callerIdentity.ServiceToken {
|
||||
s1.ExtraData["token_type"] = "service"
|
||||
} else {
|
||||
s1.ExtraData["token_type"] = "personal"
|
||||
}
|
||||
}
|
||||
}
|
||||
extraData, isVerified, verificationErr := verifyLaunchDarklyKey(ctx, s.getClient(), resMatch)
|
||||
s1.Verified = isVerified
|
||||
s1.SetVerificationError(verificationErr)
|
||||
s1.ExtraData = extraData
|
||||
|
||||
s1.AnalysisInfo = map[string]string{
|
||||
"key": resMatch,
|
||||
}
|
||||
|
||||
} else if res.StatusCode == 401 {
|
||||
// 401 is expected for an invalid token, so there is nothing to do here.
|
||||
} else {
|
||||
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
|
||||
s1.SetVerificationError(err, resMatch)
|
||||
// only api keys can be analyzed
|
||||
if strings.HasPrefix(resMatch, "api-") {
|
||||
s1.AnalysisInfo = map[string]string{
|
||||
"key": resMatch,
|
||||
}
|
||||
} else {
|
||||
s1.SetVerificationError(err, resMatch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,3 +101,57 @@ func (s Scanner) Type() detectorspb.DetectorType {
|
||||
func (s Scanner) Description() string {
|
||||
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."
|
||||
}
|
||||
|
||||
func verifyLaunchDarklyKey(ctx context.Context, client *http.Client, key string) (map[string]string, bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://app.launchdarkly.com/api/v2/caller-identity", http.NoBody)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", key)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
var callerIdentity callerIdentity
|
||||
var extraData = make(map[string]string)
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&callerIdentity); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
extraData["type"] = callerIdentity.AccountId
|
||||
extraData["account"] = callerIdentity.AccountId
|
||||
extraData["environment_id"] = callerIdentity.EnvironmentId
|
||||
extraData["project_id"] = callerIdentity.ProjectId
|
||||
extraData["environment_name"] = callerIdentity.EnvironmentName
|
||||
extraData["project_name"] = callerIdentity.ProjectName
|
||||
extraData["auth_kind"] = callerIdentity.AuthKind
|
||||
extraData["token_kind"] = callerIdentity.TokenKind
|
||||
extraData["client_id"] = callerIdentity.ClientID
|
||||
extraData["token_name"] = callerIdentity.TokenName
|
||||
extraData["member_id"] = callerIdentity.MemberId
|
||||
if callerIdentity.TokenKind == "auth" {
|
||||
if callerIdentity.ServiceToken {
|
||||
extraData["token_type"] = "service"
|
||||
} else {
|
||||
extraData["token_type"] = "personal"
|
||||
}
|
||||
}
|
||||
|
||||
return extraData, true, nil
|
||||
case http.StatusUnauthorized:
|
||||
return nil, false, nil
|
||||
default:
|
||||
return nil, false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@ func TestLaunchDarkly_FromChunk(t *testing.T) {
|
||||
t.Fatalf("no raw secret present: \n %+v", got[i])
|
||||
}
|
||||
got[i].Raw = nil
|
||||
got[i].AnalysisInfo = nil
|
||||
|
||||
// Do we expect to be comparing the ExtraData?
|
||||
got[i].ExtraData = nil
|
||||
|
||||
@@ -2,7 +2,6 @@ package launchdarkly
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -11,12 +10,6 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
|
||||
)
|
||||
|
||||
var (
|
||||
validPattern = "sdk-5gykyl1b-wgrq-41lc-z6q1-h7zn5fdlcybw"
|
||||
invalidPattern = "sdk-5gykyl1b-wgrq-41lc-z6q1-h7zn5fdlcyb"
|
||||
keyword = "launchdarkly"
|
||||
)
|
||||
|
||||
func TestLaunchDarkly_Pattern(t *testing.T) {
|
||||
d := Scanner{}
|
||||
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
|
||||
@@ -26,13 +19,23 @@ func TestLaunchDarkly_Pattern(t *testing.T) {
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "valid pattern - with keyword launchdarkly",
|
||||
input: fmt.Sprintf("%s token = '%s'", keyword, validPattern),
|
||||
want: []string{validPattern},
|
||||
name: "valid pattern - launchdarkly api key",
|
||||
input: `api-5gykyl1b-wgrq-41lc-z6q1-h7zn5fdlcybw`,
|
||||
want: []string{"api-5gykyl1b-wgrq-41lc-z6q1-h7zn5fdlcybw"},
|
||||
},
|
||||
{
|
||||
name: "invalid pattern",
|
||||
input: fmt.Sprintf("%s = '%s'", keyword, invalidPattern),
|
||||
name: "valid pattern - launchdarkly sdk key",
|
||||
input: `sdk-5gykyl1b-wgrq-41lc-z6q1-h7zn5fdlcybw`,
|
||||
want: []string{"sdk-5gykyl1b-wgrq-41lc-z6q1-h7zn5fdlcybw"},
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - invalid length",
|
||||
input: `sdk-5gykyl1b-wgrq-41lc-z6q1-h7zn5fdlcyb`,
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "invalid pattern - invalid character",
|
||||
input: `api-5Gykyl1b-Wgrq-41lC-z6q1-h7zn5fdlcybw`,
|
||||
want: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user