Files
Kashif Khan 08b7e3b4a9 Fastly Analyzer (#4082)
* initial commit

* added more apis and test cases

* fixed test cases

* added more apis

* fixed analyzer type

* Update launchdarkly_test.go
2025-05-08 12:59:16 +05:00

186 lines
4.9 KiB
Go

//go:generate generate_permissions permissions.yaml permissions.go fastly
package fastly
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"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeFastly
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, exist := credInfo["key"]
if !exist {
return nil, fmt.Errorf("key not found in credential info")
}
// analyze permissions
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
// secret info to analyzer
return secretInfoToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
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("[!] Valid Fastly API key\n\n")
if info.TokenInfo.hasGlobalScope() {
printUserInfo(info.UserInfo)
}
printScopes(info.TokenInfo.Scopes)
if len(info.Resources) > 0 {
printResources(info.Resources)
}
color.Yellow("\n[i] Expires: %s", info.TokenInfo.ExpiresAt)
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// create http client
client := analyzers.NewAnalyzeClient(cfg)
var secretInfo = &SecretInfo{}
// capture the token details
if err := captureTokenInfo(client, key, secretInfo); err != nil {
return nil, err
}
/*
Fastly defines four types of permissions. Two of these are related specifically to purging:
- If a token has either `purge_select` or `purge_all` access, it is limited to calling purge-related APIs only.
- If a token has `global` or `global:read` access, it can call APIs that retrieve resource and user information.
*/
if !secretInfo.TokenInfo.hasGlobalScope() {
return secretInfo, nil
}
// capture the user information
if err := captureUserInfo(client, key, secretInfo); err != nil {
return nil, err
}
// capture the resources
if err := captureResources(client, key, secretInfo); err != nil {
// return secretInfo as well in case of error for partial success
return secretInfo, 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.AnalyzerTypeFastly,
Metadata: map[string]any{},
Bindings: make([]analyzers.Binding, 0),
}
// extract information from resource to create bindings and append to result bindings
for _, resource := range info.Resources {
binding := analyzers.Binding{
Resource: *secretInfoResourceToAnalyzerResource(resource),
Permission: analyzers.Permission{
Value: info.TokenInfo.Scope,
},
}
if resource.Parent != nil {
binding.Resource.Parent = secretInfoResourceToAnalyzerResource(*resource.Parent)
}
result.Bindings = append(result.Bindings, binding)
}
return &result
}
// secretInfoResourceToAnalyzerResource translate secret info resource to analyzer resource for binding
func secretInfoResourceToAnalyzerResource(resource FastlyResource) *analyzers.Resource {
analyzerRes := analyzers.Resource{
// make fully qualified name unique
FullyQualifiedName: resource.Type + "/" + resource.ID,
Name: resource.Name,
Type: resource.Type,
Metadata: map[string]any{},
}
for key, value := range resource.Metadata {
analyzerRes.Metadata[key] = value
}
return &analyzerRes
}
// cli print functions
func printUserInfo(user User) {
color.Yellow("[i] User Information:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"ID", "Name", "Login", "Role", "Last Active At"})
t.AppendRow(table.Row{color.GreenString(user.ID), color.GreenString(user.Name), color.GreenString(user.Login), color.GreenString(user.Role), color.GreenString(user.LastActiveAt)})
t.Render()
}
func printScopes(scopes []string) {
color.Yellow("[i] Scopes:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scopes"})
for _, scope := range scopes {
t.AppendRow(table.Row{color.GreenString(scope)})
}
t.Render()
}
func printResources(resources []FastlyResource) {
color.Yellow("[i] Resources:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Name", "Type"})
for _, resource := range resources {
t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
}
t.Render()
}