//go:generate generate_permissions permissions.yaml permissions.go datadog package datadog import ( "errors" "fmt" "net/url" "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" ) var _ analyzers.Analyzer = (*Analyzer)(nil) type Analyzer struct { Cfg *config.Config } func (a Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeDatadog } // Analyze performs the analysis of the Datadog API key and returns the analyzer result. func (a Analyzer) Analyze(ctx context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) { apiKey := credInfo["api_key"] appKey := credInfo["app_key"] endpoint := credInfo["endpoint"] info, err := AnalyzePermissions(a.Cfg, apiKey, appKey, endpoint) if err != nil { return nil, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationAnalyzePermissions, analyzers.ServiceAPI, "", err) } return secretInfoToAnalyzerResult(info), nil } func AnalyzeAndPrintPermissions(cfg *config.Config, apiKey, appKey, endpoint string) { info, err := AnalyzePermissions(cfg, apiKey, appKey, endpoint) 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] No information retrieved") return } color.Green("[i] Valid Datadog API Key\n") printUser(info.User) printResources(info.Resources) printPermissions(info.Permissions) } // AnalyzePermissions will collect all the scopes assigned to token along with resource it can access func AnalyzePermissions(cfg *config.Config, apiKey, appKey, endpoint string) (*SecretInfo, error) { if apiKey == "" { return nil, errors.New("api key not found in credentials info") } // create the http client client := analyzers.NewAnalyzeClient(cfg) var secretInfo = &SecretInfo{} var baseURL string var err error // If endpoint is provided, use it directly; otherwise detect domain if endpoint != "" { baseURL, err = url.JoinPath(endpoint, "api") if err != nil { return nil, fmt.Errorf("failed to join path: %w", err) } } else { baseURL, err = DetectDomain(client, apiKey, appKey) if err != nil { return nil, fmt.Errorf("[x] %v", err) } } if appKey == "" { // If no application key is provided, we can only validate the API key if endpoint != "" { // If endpoint is empty we don't need to validate again because DetectDomain would have already validated the API key against detected domain // But if endpoint is provided, we should validate the API key against the provided endpoint to ensure it's valid before proceeding isValidApiKey, err := ValidateApiKey(client, baseURL, apiKey) if err != nil { return nil, fmt.Errorf("failed to validate api key: %v", err) } if !isValidApiKey { return nil, errors.New("invalid api key provided") } } if err := CaptureApiKeyPermissions(secretInfo); err != nil { return nil, fmt.Errorf("failed to fetch permissions: %v", err) } return secretInfo, nil } // capture user information in secretInfo // If the application key is scoped, user information cannot be retrieved even if all the permissions are granted // This is a non-documented Endpoint and can lead to unexpected behavior in future updates // If user information is not retrieved, we will move ahead with the rest of the analysis and print the error _ = CaptureUserInformation(client, baseURL, apiKey, appKey, secretInfo) // capture resources in secretInfo if err := CaptureResources(client, baseURL, apiKey, appKey, secretInfo); err != nil { return nil, fmt.Errorf("failed to fetch resources: %v", err) } // capture permissions in secretInfo if err := CapturePermissions(client, baseURL, apiKey, appKey, secretInfo); err != nil { return nil, fmt.Errorf("failed to fetch permissions: %v", err) } // Capture API key permissions if err := CaptureApiKeyPermissions(secretInfo); err != nil { return nil, fmt.Errorf("failed to fetch permissions: %v", err) } return secretInfo, nil } // secretInfoToAnalyzerResult translate secret info to Analyzer Result func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult { if info == nil { return nil } result := analyzers.AnalyzerResult{ AnalyzerType: analyzers.AnalyzerTypeDatadog, Metadata: map[string]any{}, Bindings: make([]analyzers.Binding, 0), } // Create user resource to use as parent var userResource *analyzers.Resource if info.User.Id != "" { userResource = &analyzers.Resource{ FullyQualifiedName: info.User.Id, Name: info.User.Name, Type: "User", Metadata: map[string]any{ "email": info.User.Email, }, } } permissionBindings := secretInfoPermissionsToAnalyzerPermission(info.Permissions) if userResource != nil && len(*permissionBindings) > 0 { result.Bindings = analyzers.BindAllPermissions(*userResource, *permissionBindings...) } if userResource == nil && len(*permissionBindings) > 0 { result.Bindings = analyzers.BindAllPermissions(analyzers.Resource{ FullyQualifiedName: "Unknown User", Name: "Unknown User", Type: "User", Metadata: map[string]any{}, }, *permissionBindings...) } // Extract information from resources to create bindings for _, resource := range info.Resources { resource := secretInfoResourceToAnalyzerResource(resource) // Set the user resource as parent if available if userResource != nil { resource.Parent = userResource } binding := analyzers.Binding{ Resource: *resource, } result.Bindings = append(result.Bindings, binding) } return &result } // secretInfoPermissionsToAnalyzerPermission translate secret info Permission to analyzer resource for binding func secretInfoPermissionsToAnalyzerPermission(perms []Permission) *[]analyzers.Permission { permissions := make([]analyzers.Permission, 0, len(perms)) for _, perm := range perms { permissions = append(permissions, analyzers.Permission{ Value: perm.Title, }) } return &permissions } // secretInfoResourceToAnalyzerResource translate secret info Resource to analyzer resource for binding func secretInfoResourceToAnalyzerResource(resource Resource) *analyzers.Resource { analyzerRes := analyzers.Resource{ FullyQualifiedName: resource.ID, Name: resource.Name, Type: resource.Type, Metadata: map[string]any{}, } for key, value := range resource.MetaData { analyzerRes.Metadata[key] = value } return &analyzerRes } func printUser(user User) { if user.Id == "" { color.Red("\n[x] User information not available") return } color.Green("\n[i] User Information:") userTable := table.NewWriter() userTable.SetOutputMirror(os.Stdout) userTable.AppendHeader(table.Row{"User Id", "Name", "Email"}) userTable.AppendRow(table.Row{color.GreenString(user.Id), color.GreenString(user.Name), color.GreenString(user.Email)}) userTable.Render() } func printResources(resources []Resource) { if len(resources) == 0 { color.Red("[x] No resources found") return } color.Green("\n[i] Resources:") resourceTable := table.NewWriter() resourceTable.SetOutputMirror(os.Stdout) resourceTable.AppendHeader(table.Row{"Name", "Type"}) for _, resource := range resources { resourceTable.AppendRow(table.Row{ color.GreenString(resource.Name), color.GreenString(resource.Type), }) } resourceTable.Render() } func printPermissions(permissions []Permission) { if len(permissions) == 0 { color.Red("[x] No permissions found") return } color.Green("\n[i] Permissions:") permissionTable := table.NewWriter() permissionTable.SetOutputMirror(os.Stdout) permissionTable.AppendHeader(table.Row{"Title", "Name", "Description"}) // Set wrapping for long descriptions permissionTable.SetColumnConfigs([]table.ColumnConfig{ {Number: 3, WidthMax: 50}, }) for _, permission := range permissions { permissionTable.AppendRow(table.Row{ color.GreenString(permission.Title), color.GreenString(permission.Name), color.GreenString(permission.Description), }) } permissionTable.Render() }