Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LaunchDarkly Token Analyzer #3948

Merged
Merged
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
36e64af
initial commit
kashifkhan0771 Feb 21, 2025
1c92298
initial commit
kashifkhan0771 Feb 24, 2025
f143fa8
initial commit
kashifkhan0771 Feb 25, 2025
b840782
inital commit
kashifkhan0771 Feb 26, 2025
aab2c0a
initial working structure for launchdarkly analyzer
kashifkhan0771 Feb 27, 2025
9806026
added more apis
kashifkhan0771 Mar 4, 2025
cc2ac00
Merge branch 'main' into feat/oss-95-launchdarkly-analyzer
kashifkhan0771 Mar 4, 2025
8ff00ae
added test cases
kashifkhan0771 Mar 4, 2025
745868e
removed imposter print statement
kashifkhan0771 Mar 4, 2025
6d3b8fa
updated some code
kashifkhan0771 Mar 4, 2025
6902b03
Merge branch 'main' into feat/oss-95-launchdarkly-analyzer
kashifkhan0771 Mar 5, 2025
746e4fb
removed id from printResources
kashifkhan0771 Mar 5, 2025
8452bdd
added nabeel suggestion and set analysis info
kashifkhan0771 Mar 5, 2025
d445b25
Merge branch 'main' into feat/oss-95-launchdarkly-analyzer
kashifkhan0771 Mar 5, 2025
25356be
Merge branch 'main' into feat/oss-95-launchdarkly-analyzer
kashifkhan0771 Mar 6, 2025
4235470
Merge branch 'main' into feat/oss-95-launchdarkly-analyzer
kashifkhan0771 Mar 6, 2025
143947a
Merge branch 'main' into feat/oss-95-launchdarkly-analyzer
kashifkhan0771 Mar 7, 2025
8a0561a
resolved ahrav comments
kashifkhan0771 Mar 7, 2025
deccdd9
Merge branch 'main' into feat/oss-95-launchdarkly-analyzer
kashifkhan0771 Mar 7, 2025
8d6265f
Merge branch 'main' into feat/oss-95-launchdarkly-analyzer
kashifkhan0771 Mar 10, 2025
08cdc2f
resolved ahrav comments
kashifkhan0771 Mar 10, 2025
47cdd0c
implemented ahrav's suggestion 🔥
kashifkhan0771 Mar 10, 2025
1c61fce
resolved linter error
kashifkhan0771 Mar 10, 2025
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
Prev Previous commit
Next Next commit
initial commit
  • Loading branch information
kashifkhan0771 committed Feb 25, 2025
commit 1c92298208755b746fa3f0466275863bd25535e4
22 changes: 0 additions & 22 deletions pkg/analyzer/analyzers/launchdarkly/api_endpoints.json

This file was deleted.

214 changes: 214 additions & 0 deletions pkg/analyzer/analyzers/launchdarkly/callerIdentity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*
callerIdentity.go file is all related to calling APIs to get caller and token information and formatting them to secretInfo CallerIdentity.

It calls 3 APIs:
- /v2/caller-identity
- /v2/tokens/<id> (with token id from previous api response)
- /v2/roles/<role_id> (if custom role id is present in tokens) (more than one role can be assigned to token as well)

it formats all these responses into one CallerIdentity struct for secretInfo.
*/
package launchdarkly

import (
"encoding/json"
"fmt"
"net/http"
)

// callerIdentityResponse is /v2/caller-identity API response
type callerIdentityResponse struct {
AccountID string `json:"accountId"`
TokenName string `json:"tokenName"`
TokenID string `json:"tokenId"`
MemberID string `json:"memberId"`
ServiceToken bool `json:"serviceToken"`
}

// tokenResponse is the /v2/tokens/<id> API response
type tokenResponse struct {
OwnerID string `json:"ownerId"`
Member tokenMemberResponse `json:"_member"`
Name string `json:"name"`
CustomRoleIDs []string `json:"customRoleIds"`
InlineRole tokenPolicyResponse `json:"inlineRole"`
Role string `json:"role"`
ServiceToken bool `json:"serviceToken"`
DefaultAPIVersion int `json:"defaultApiVersion"`
}

// _member object in token response
type tokenMemberResponse struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Role string `json:"role"`
Email string `json:"email"`
}

// inlineRole object in token response
type tokenPolicyResponse struct {
Effect string `json:"effect"`
Resources []string `json:"resources"`
NotResources []string `json:"notResources"`
Actions []string `json:"actions"`
NotActions []string `json:"notActions"`
}

// customRoleResponse is the /v2/roles/<role_id> API response
type customRoleResponse struct {
ID string `json:"_id"`
Key string `json:"key"`
Name string `json:"name"`
Policy []tokenPolicyResponse `json:"policy"`
BasePermission string `json:"basePermissions"`
AssignedTo struct {
MembersCount int `json:"membersCount"`
TeamsCount int `json:"teamsCount"`
} `json:"assignedTo"`
}

/*
fetchCallerDetails call following three APIs:
- /v2/caller-identity
- /v2/tokens/<token_id> (token_id from previous API response)
- /v2/roles/<role_id> (roles_id from previous API response if exist)

It format all responses into one secret info CallerIdentity
*/
func fetchCallerDetails(client *http.Client, token string) (*CallerIdentity, error) {
response, statusCode, err := makeLaunchDarklyRequest(client, endpoints["callerIdentity"], token)
if err != nil {
return nil, err
}

switch statusCode {
case http.StatusOK:
var caller callerIdentityResponse

if err := json.Unmarshal(response, &caller); err != nil {
return nil, err
}

tokenDetails, err := getToken(client, caller.TokenID, token)
if err != nil {
return nil, err
}

customRoles, err := getCustomRole(client, tokenDetails.CustomRoleIDs, token)
if err != nil {
return nil, err
}

return makeCallerIdentity(caller, *tokenDetails, customRoles), nil
case http.StatusUnauthorized:
return nil, nil
default:
return nil, fmt.Errorf("unexpected status code: %d", statusCode)
}
}

// getToken call /v2/tokens/<token_id> API and return response
func getToken(client *http.Client, tokenID, token string) (*tokenResponse, error) {
response, statusCode, err := makeLaunchDarklyRequest(client, fmt.Sprintf(endpoints["getToken"], tokenID), token)
if err != nil {
return nil, err
}

switch statusCode {
case http.StatusOK:
var token tokenResponse

if err := json.Unmarshal(response, &token); err != nil {
return nil, err
}

return &token, nil
case http.StatusUnauthorized:
return nil, nil
default:
return nil, fmt.Errorf("unexpected status code: %d", statusCode)
}
}

// getCustomRole call /v2/roles/<role_id> API for all IDs passed and return list of responses
func getCustomRole(client *http.Client, customRoleIDs []string, token string) ([]customRoleResponse, error) {
var customRoles []customRoleResponse

for _, customRoleID := range customRoleIDs {
response, statusCode, err := makeLaunchDarklyRequest(client, fmt.Sprintf(endpoints["getRole"], customRoleID), token)
if err != nil {
return nil, err
}

switch statusCode {
case http.StatusOK:
var customRole customRoleResponse

if err := json.Unmarshal(response, &customRole); err != nil {
return nil, err
}

customRoles = append(customRoles, customRole)
case http.StatusUnauthorized:
return nil, nil
default:
return nil, fmt.Errorf("unexpected status code: %d", statusCode)
}
}

return customRoles, nil
}

// makeCallerIdentity take caller, tokenDetails, and customRoles and return secret info CallerIdentity
func makeCallerIdentity(caller callerIdentityResponse, tokenDetails tokenResponse, customRoles []customRoleResponse) *CallerIdentity {
return &CallerIdentity{
AccountID: caller.AccountID,
MemberID: caller.MemberID,
Name: tokenDetails.Member.FirstName + " " + tokenDetails.Member.LastName,
Role: tokenDetails.Member.Role,
Email: tokenDetails.Member.Email,
Token: Token{
ID: caller.TokenID,
Name: tokenDetails.Name,
Role: tokenDetails.Role,
APIVersion: tokenDetails.DefaultAPIVersion,
IsServiceToken: tokenDetails.ServiceToken,
InlineRole: toPolicy(tokenDetails.InlineRole),
CustomRoles: toCustomRoles(customRoles),
},
}
}

// toPolicy convert inlinePolicy from token response to secret info caller identity policy
func toPolicy(inlinePolices ...tokenPolicyResponse) []Policy {
var policies = make([]Policy, len(inlinePolices))
for _, inlinePolicy := range inlinePolices {
policies = append(policies, Policy{
Resources: inlinePolicy.Resources,
NotResources: inlinePolicy.NotResources,
Actions: inlinePolicy.Actions,
NotActions: inlinePolicy.NotActions,
Effect: inlinePolicy.Effect,
})
}

return policies
}

// toCustomRoles convert customRole from token response to secret info caller identity custom role
func toCustomRoles(roles []customRoleResponse) []CustomRole {
var customRoles = make([]CustomRole, len(roles))
for _, role := range roles {
customRoles = append(customRoles, CustomRole{
ID: role.ID,
Key: role.Key,
Name: role.Name,
Polices: toPolicy(role.Policy...),
BasePermission: role.BasePermission,
AssignedToMembers: role.AssignedTo.MembersCount,
AssignedToTeams: role.AssignedTo.TeamsCount,
})
}

return customRoles
}
56 changes: 56 additions & 0 deletions pkg/analyzer/analyzers/launchdarkly/launchdarkly.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package launchdarkly

import (
"fmt"
"os"

"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"

"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
@@ -20,12 +26,62 @@ func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analy
return nil, nil
}

func AnalyzeAndPrintPermissions(cfg *config.Config, token string) {
info, err := AnalyzePermissions(cfg, token)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Error : %s", err.Error())
}

if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}

color.Green("[i] Valid LaunchDarkly Token\n")
printCallerIdentity(info.CallerIdentity)

color.Yellow("\n[!] Expires: Never")
}

// AnalyzePermissions will collect all the scopes assigned to token along with resource it can access
func AnalyzePermissions(cfg *config.Config, token string) (*SecretInfo, error) {
// create the http client
client := analyzers.NewAnalyzeClient(cfg)

var secretInfo = &SecretInfo{}

// get caller identity
callerIdentity, err := fetchCallerDetails(client, token)
if err != nil {
return nil, fmt.Errorf("failed to fetch caller identity: %v", err)
}

if callerIdentity != nil {
secretInfo.CallerIdentity = *callerIdentity
}

return secretInfo, nil
}

func printCallerIdentity(caller CallerIdentity) {
// print caller information
color.Green("\n[i] Caller:")
callerTable := table.NewWriter()
callerTable.SetOutputMirror(os.Stdout)
callerTable.AppendHeader(table.Row{"Account ID", "Member ID", "Name", "Email", "Role"})
callerTable.AppendRow(table.Row{color.GreenString(caller.AccountID), color.GreenString(caller.MemberID),
color.GreenString(caller.Name), color.GreenString(caller.Email), color.GreenString(caller.Role)})

callerTable.Render()

// print token information
color.Green("\n[i] Token")
tokenTable := table.NewWriter()
tokenTable.SetOutputMirror(os.Stdout)
tokenTable.AppendHeader(table.Row{"ID", "Name", "Role", "Is Service Token", "Default API Version"})
tokenTable.AppendRow(table.Row{color.GreenString(caller.Token.ID), color.GreenString(caller.Token.Name), color.GreenString(caller.Token.Role),
color.GreenString(fmt.Sprintf("%t", caller.Token.IsServiceToken)), color.GreenString(fmt.Sprintf("%d", caller.Token.APIVersion))})

tokenTable.Render()
}
21 changes: 12 additions & 9 deletions pkg/analyzer/analyzers/launchdarkly/models.go
Original file line number Diff line number Diff line change
@@ -17,15 +17,18 @@ type CallerIdentity struct {
Name string
Role string // role of caller
Email string
Token struct {
ID string // id of the token
Name string // name of the token
CustomRoles []CustomRole // custom roles assigned to the token
InlineRole []Policy // any policy statements maybe used in place of a built-in custom role
Role string // role of token
IsServiceToken bool // is a service token or not
APIVersion int // default api version assigned to the token
}
Token Token
}

// Token is the token details
type Token struct {
ID string // id of the token
Name string // name of the token
CustomRoles []CustomRole // custom roles assigned to the token
InlineRole []Policy // any policy statements maybe used in place of a built-in custom role
Role string // role of token
IsServiceToken bool // is a service token or not
APIVersion int // default api version assigned to the token
}

// CustomRole is a flexible policies providing fine-grained access control to everything in launch darkly
Loading
Oops, something went wrong.
You are viewing a condensed version of this merge commit. You can view the full changes here.