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 26, 2025
commit f143fa8160528fa4b05e8647b03039dd77af5774
2 changes: 2 additions & 0 deletions pkg/analyzer/analyzers/analyzers.go
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import (
"sort"

"github.com/fatih/color"

"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)

@@ -98,6 +99,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
AnalyzerAnthropic: "Anthropic",
AnalyzerTypeAsana: "Asana",
AnalyzerTypeBitbucket: "Bitbucket",
AnalyzerTypeDigitalOcean: "DigitalOcean",
AnalyzerTypeDockerHub: "DockerHub",
AnalyzerTypeGitHub: "GitHub",
AnalyzerTypeGitLab: "GitLab",
125 changes: 110 additions & 15 deletions pkg/analyzer/analyzers/launchdarkly/launchdarkly.go
Original file line number Diff line number Diff line change
@@ -39,7 +39,8 @@ func AnalyzeAndPrintPermissions(cfg *config.Config, token string) {
}

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

color.Yellow("\n[!] Expires: Never")
}
@@ -52,36 +53,130 @@ func AnalyzePermissions(cfg *config.Config, token string) (*SecretInfo, error) {
var secretInfo = &SecretInfo{}

// get caller identity
callerIdentity, err := fetchCallerDetails(client, token)
if err != nil {
if err := FetchUserInformation(client, token, secretInfo); 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) {
// printUser print User information from secret info to cli
func printUser(user User) {
// print caller information
color.Green("\n[i] Caller:")
color.Green("\n[i] User Information:")
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.AppendRow(table.Row{color.GreenString(user.AccountID), color.GreenString(user.MemberID),
color.GreenString(user.Name), color.GreenString(user.Email), color.GreenString(user.Role)})

callerTable.Render()

// print token information
color.Green("\n[i] Token")
color.Green("\n[i] Token Information")
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.AppendHeader(table.Row{"ID", "Name", "Role", "Is Service Token", "Default API Version",
"No of Custom Roles Assigned", "Has Inline Policy"})

tokenTable.AppendRow(table.Row{color.GreenString(user.Token.ID), color.GreenString(user.Token.Name), color.GreenString(user.Token.Role),
color.GreenString(fmt.Sprintf("%t", user.Token.IsServiceToken)), color.GreenString(fmt.Sprintf("%d", user.Token.APIVersion)),
color.GreenString(fmt.Sprintf("%d", len(user.Token.CustomRoles))), color.GreenString(fmt.Sprintf("%t", user.Token.hasInlineRole()))})

tokenTable.Render()

// print custom roles information
if user.Token.hasCustomRoles() {
// print token information
color.Green("\n[i] Custom Roles Assigned to Token")
rolesTable := table.NewWriter()
rolesTable.SetOutputMirror(os.Stdout)
rolesTable.AppendHeader(table.Row{"ID", "Key", "Name", "Base Permission", "Assigned to members", "Assigned to teams"})
for _, customRole := range user.Token.CustomRoles {
rolesTable.AppendRow(table.Row{color.GreenString(customRole.ID), color.GreenString(customRole.Key), color.GreenString(customRole.Name),
color.GreenString(customRole.BasePermission), color.GreenString(fmt.Sprintf("%d", customRole.AssignedToMembers)),
color.GreenString(fmt.Sprintf("%d", customRole.AssignedToTeams))})
}
rolesTable.Render()
}
}

// printPermissionType print type of permission token has
func printPermissionsType(token Token) {
// print permission type. It can be either admin, writer, reader or has inline policy or any custom roles assigned
permission := ""

if token.Role != "" {
permission = token.Role
} else if token.hasInlineRole() {
permission = "Inline Policy"
} else if token.hasCustomRoles() {
permission = "Custom Roles"
}

color.Green("\n[i] Permission Type: %s", permission)
policesTable := table.NewWriter()
policesTable.SetOutputMirror(os.Stdout)
policesTable.AppendHeader(table.Row{"Resource (* means all)", "Action", "Effect"})
permissions := GetTokenPermissions(token)
for resource, actions := range permissions {
for action, effect := range actions {
if effect == "allow" {
policesTable.AppendRow(table.Row{color.GreenString(resource), color.GreenString(action), color.GreenString(effect)})
} else {
policesTable.AppendRow(table.Row{color.YellowString(resource), color.YellowString(action), color.YellowString(effect)})
}
}
}

policesTable.Render()
}

// GetTokenPermissions returns a mapping of allowed and denied actions with resources and effects.
func GetTokenPermissions(token Token) map[string]map[string]string {
permissions := make(map[string]map[string]string)

// Process Inline Role
for _, policy := range token.InlineRole {
processPolicy(policy, permissions)
}

// Process Custom Roles
for _, role := range token.CustomRoles {
for _, policy := range role.Polices {
processPolicy(policy, permissions)
}
}

return permissions
}

// processPolicy updates the permissions map with policy details
func processPolicy(policy Policy, permissions map[string]map[string]string) {
// Handle allowed actions
for _, resource := range policy.Resources {
if _, exists := permissions[resource]; !exists {
permissions[resource] = make(map[string]string)
}
for _, action := range policy.Actions {
permissions[resource][action] = policy.Effect
}
for _, action := range policy.NotActions {
permissions[resource][action] = policy.Effect
}
}

// Handle denied actions
for _, resource := range policy.NotResources {
if _, exists := permissions[resource]; !exists {
permissions[resource] = make(map[string]string)
}
for _, action := range policy.Actions {
permissions[resource][action] = policy.Effect
}
for _, action := range policy.NotActions {
permissions[resource][action] = policy.Effect
}
}
}
18 changes: 9 additions & 9 deletions pkg/analyzer/analyzers/launchdarkly/models.go
Original file line number Diff line number Diff line change
@@ -3,15 +3,15 @@ package launchdarkly
import "sync"

type SecretInfo struct {
CallerIdentity CallerIdentity
Permissions []string
Resources []Resource
User User
Permissions []string
Resources []Resource
// to concurrently read and write
mu sync.RWMutex
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I've found it helpful to use mu almost like a "hat" for the resource(s) it protects by placing it directly above them. This makes it visually clear what it's guarding and can even replace the need for a comment.

That said, I’m not entirely sure which resources mu is protecting in this case. 🤔 This is definitely an opinionated take, so feel free to ignore it! 🤣

I forget where I first read about this, but here is a separate article about it. 😅

Ex:

type SecretInfo struct {
    ...
    mu sync.RWMutex <-- see, like a "hat" for "Resources"
    Resources []Resource
}

}

// CallerIdentity is the information about the token
type CallerIdentity struct {
// User is the information about the user to whom the token belongs
type User struct {
AccountID string // account id. It is the owner id of token as well
MemberID string
Name string
@@ -91,13 +91,13 @@ func (s *SecretInfo) appendResource(resource Resource) {
}

// hasCustomRoles check if token has any custom roles assigned
func (c CallerIdentity) hasCustomRoles() bool {
return len(c.Token.CustomRoles) > 0
func (t Token) hasCustomRoles() bool {
return len(t.CustomRoles) > 0
}

// hasInlineRole check if token has any inline roles
func (c CallerIdentity) hasInlineRole() bool {
return len(c.Token.InlineRole) > 0
func (t Token) hasInlineRole() bool {
return len(t.InlineRole) > 0
}

// isAllowed check if policy allow the statement
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
/*
callerIdentity.go file is all related to calling APIs to get caller and token information and formatting them to secretInfo CallerIdentity.
user.go file is all related to calling APIs to get user and token information and formatting them to secretInfo User.

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.
it formats all these responses into one User struct for secretInfo.
*/
package launchdarkly

import (
"encoding/json"
"errors"
"fmt"
"net/http"
)
@@ -27,14 +28,14 @@ type callerIdentityResponse struct {

// 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"`
OwnerID string `json:"ownerId"`
Member tokenMemberResponse `json:"_member"`
Name string `json:"name"`
CustomRoleIDs []string `json:"customRoleIds,omitempty"`
InlineRole []tokenPolicyResponse `json:"inlineRole,omitempty"`
Role string `json:"role"`
ServiceToken bool `json:"serviceToken"`
DefaultAPIVersion int `json:"defaultApiVersion"`
}

// _member object in token response
@@ -47,11 +48,11 @@ type tokenMemberResponse struct {

// 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"`
Effect string `json:"effect,omitempty"`
Resources []string `json:"resources,omitempty"`
NotResources []string `json:"notResources,omitempty"`
Actions []string `json:"actions,omitempty"`
NotActions []string `json:"notActions,omitempty"`
}

// customRoleResponse is the /v2/roles/<role_id> API response
@@ -68,40 +69,52 @@ type customRoleResponse struct {
}

/*
fetchCallerDetails call following three APIs:
FetchUserInformation 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
It format all responses into one secret info User
*/
func fetchCallerDetails(client *http.Client, token string) (*CallerIdentity, error) {
func FetchUserInformation(client *http.Client, token string, secretInfo *SecretInfo) error {
caller, err := getCallerIdentity(client, token)
if err != nil {
return err
}

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

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

addUserToSecretInfo(caller, tokenDetails, customRoles, secretInfo)

return nil
}

// getCallerIdentity call /v2/caller-identity API and return response
func getCallerIdentity(client *http.Client, token string) (*callerIdentityResponse, error) {
response, statusCode, err := makeLaunchDarklyRequest(client, endpoints["callerIdentity"], token)
if err != nil {
return nil, err
}

switch statusCode {
case http.StatusOK:
var caller callerIdentityResponse
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
if err := json.Unmarshal(response, caller); err != nil {
return caller, err
}

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

return makeCallerIdentity(caller, *tokenDetails, customRoles), nil
return caller, nil
case http.StatusUnauthorized:
return nil, nil
return nil, errors.New("invalid token; failed to get caller information")
default:
return nil, fmt.Errorf("unexpected status code: %d", statusCode)
}
@@ -124,7 +137,7 @@ func getToken(client *http.Client, tokenID, token string) (*tokenResponse, error

return &token, nil
case http.StatusUnauthorized:
return nil, nil
return nil, errors.New("invalid token; failed to get token information")
default:
return nil, fmt.Errorf("unexpected status code: %d", statusCode)
}
@@ -160,8 +173,8 @@ func getCustomRole(client *http.Client, customRoleIDs []string, token string) ([
}

// makeCallerIdentity take caller, tokenDetails, and customRoles and return secret info CallerIdentity
func makeCallerIdentity(caller callerIdentityResponse, tokenDetails tokenResponse, customRoles []customRoleResponse) *CallerIdentity {
return &CallerIdentity{
func addUserToSecretInfo(caller *callerIdentityResponse, tokenDetails *tokenResponse, customRoles []customRoleResponse, secretInfo *SecretInfo) {
user := User{
AccountID: caller.AccountID,
MemberID: caller.MemberID,
Name: tokenDetails.Member.FirstName + " " + tokenDetails.Member.LastName,
@@ -177,11 +190,14 @@ func makeCallerIdentity(caller callerIdentityResponse, tokenDetails tokenRespons
CustomRoles: toCustomRoles(customRoles),
},
}

secretInfo.User = user
}

// toPolicy convert inlinePolicy from token response to secret info caller identity policy
func toPolicy(inlinePolices ...tokenPolicyResponse) []Policy {
var policies = make([]Policy, len(inlinePolices))
func toPolicy(inlinePolices []tokenPolicyResponse) []Policy {
var policies = make([]Policy, 0)

for _, inlinePolicy := range inlinePolices {
policies = append(policies, Policy{
Resources: inlinePolicy.Resources,
@@ -197,13 +213,13 @@ func toPolicy(inlinePolices ...tokenPolicyResponse) []Policy {

// toCustomRoles convert customRole from token response to secret info caller identity custom role
func toCustomRoles(roles []customRoleResponse) []CustomRole {
var customRoles = make([]CustomRole, len(roles))
var customRoles = make([]CustomRole, 0)
for _, role := range roles {
customRoles = append(customRoles, CustomRole{
ID: role.ID,
Key: role.Key,
Name: role.Name,
Polices: toPolicy(role.Policy...),
Polices: toPolicy(role.Policy),
BasePermission: role.BasePermission,
AssignedToMembers: role.AssignedTo.MembersCount,
AssignedToTeams: role.AssignedTo.TeamsCount,
Loading
Oops, something went wrong.
You are viewing a condensed version of this merge commit. You can view the full changes here.