Skip to content

Commit 08b7e3b

Browse files
Fastly Analyzer (#4082)
* initial commit * added more apis and test cases * fixed test cases * added more apis * fixed analyzer type * Update launchdarkly_test.go
1 parent 425c343 commit 08b7e3b

File tree

11 files changed

+1645
-9
lines changed

11 files changed

+1645
-9
lines changed

pkg/analyzer/analyzers/analyzers.go

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const (
9595
AnalyzerTypeFigma
9696
AnalyzerTypePlaid
9797
AnalyzerTypeNetlify
98+
AnalyzerTypeFastly
9899
// Add new items here with AnalyzerType prefix
99100
)
100101

@@ -135,6 +136,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
135136
AnalyzerTypeFigma: "Figma",
136137
AnalyzerTypePlaid: "Plaid",
137138
AnalyzerTypeNetlify: "Netlify",
139+
AnalyzerTypeFastly: "Fastly",
138140
// Add new mappings here
139141
}
140142

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
//go:generate generate_permissions permissions.yaml permissions.go fastly
2+
package fastly
3+
4+
import (
5+
"fmt"
6+
"os"
7+
8+
"github.com/fatih/color"
9+
"github.com/jedib0t/go-pretty/v6/table"
10+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
13+
)
14+
15+
var _ analyzers.Analyzer = (*Analyzer)(nil)
16+
17+
type Analyzer struct {
18+
Cfg *config.Config
19+
}
20+
21+
func (a Analyzer) Type() analyzers.AnalyzerType {
22+
return analyzers.AnalyzerTypeFastly
23+
}
24+
25+
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
26+
key, exist := credInfo["key"]
27+
if !exist {
28+
return nil, fmt.Errorf("key not found in credential info")
29+
}
30+
31+
// analyze permissions
32+
info, err := AnalyzePermissions(a.Cfg, key)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
// secret info to analyzer
38+
return secretInfoToAnalyzerResult(info), nil
39+
}
40+
41+
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
42+
info, err := AnalyzePermissions(cfg, key)
43+
if err != nil {
44+
// just print the error in cli and continue as a partial success
45+
color.Red("[x] Error : %s", err.Error())
46+
}
47+
48+
if info == nil {
49+
color.Red("[x] Error : %s", "No information found")
50+
return
51+
}
52+
53+
color.Green("[!] Valid Fastly API key\n\n")
54+
55+
if info.TokenInfo.hasGlobalScope() {
56+
printUserInfo(info.UserInfo)
57+
}
58+
59+
printScopes(info.TokenInfo.Scopes)
60+
61+
if len(info.Resources) > 0 {
62+
printResources(info.Resources)
63+
}
64+
65+
color.Yellow("\n[i] Expires: %s", info.TokenInfo.ExpiresAt)
66+
}
67+
68+
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
69+
// create http client
70+
client := analyzers.NewAnalyzeClient(cfg)
71+
72+
var secretInfo = &SecretInfo{}
73+
74+
// capture the token details
75+
if err := captureTokenInfo(client, key, secretInfo); err != nil {
76+
return nil, err
77+
}
78+
79+
/*
80+
Fastly defines four types of permissions. Two of these are related specifically to purging:
81+
82+
- If a token has either `purge_select` or `purge_all` access, it is limited to calling purge-related APIs only.
83+
- If a token has `global` or `global:read` access, it can call APIs that retrieve resource and user information.
84+
*/
85+
86+
if !secretInfo.TokenInfo.hasGlobalScope() {
87+
return secretInfo, nil
88+
}
89+
90+
// capture the user information
91+
if err := captureUserInfo(client, key, secretInfo); err != nil {
92+
return nil, err
93+
}
94+
95+
// capture the resources
96+
if err := captureResources(client, key, secretInfo); err != nil {
97+
// return secretInfo as well in case of error for partial success
98+
return secretInfo, err
99+
}
100+
101+
return secretInfo, nil
102+
}
103+
104+
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
105+
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
106+
if info == nil {
107+
return nil
108+
}
109+
110+
result := analyzers.AnalyzerResult{
111+
AnalyzerType: analyzers.AnalyzerTypeFastly,
112+
Metadata: map[string]any{},
113+
Bindings: make([]analyzers.Binding, 0),
114+
}
115+
116+
// extract information from resource to create bindings and append to result bindings
117+
for _, resource := range info.Resources {
118+
binding := analyzers.Binding{
119+
Resource: *secretInfoResourceToAnalyzerResource(resource),
120+
Permission: analyzers.Permission{
121+
Value: info.TokenInfo.Scope,
122+
},
123+
}
124+
125+
if resource.Parent != nil {
126+
binding.Resource.Parent = secretInfoResourceToAnalyzerResource(*resource.Parent)
127+
}
128+
129+
result.Bindings = append(result.Bindings, binding)
130+
131+
}
132+
133+
return &result
134+
}
135+
136+
// secretInfoResourceToAnalyzerResource translate secret info resource to analyzer resource for binding
137+
func secretInfoResourceToAnalyzerResource(resource FastlyResource) *analyzers.Resource {
138+
analyzerRes := analyzers.Resource{
139+
// make fully qualified name unique
140+
FullyQualifiedName: resource.Type + "/" + resource.ID,
141+
Name: resource.Name,
142+
Type: resource.Type,
143+
Metadata: map[string]any{},
144+
}
145+
146+
for key, value := range resource.Metadata {
147+
analyzerRes.Metadata[key] = value
148+
}
149+
150+
return &analyzerRes
151+
}
152+
153+
// cli print functions
154+
func printUserInfo(user User) {
155+
color.Yellow("[i] User Information:")
156+
t := table.NewWriter()
157+
t.SetOutputMirror(os.Stdout)
158+
t.AppendHeader(table.Row{"ID", "Name", "Login", "Role", "Last Active At"})
159+
t.AppendRow(table.Row{color.GreenString(user.ID), color.GreenString(user.Name), color.GreenString(user.Login), color.GreenString(user.Role), color.GreenString(user.LastActiveAt)})
160+
161+
t.Render()
162+
}
163+
164+
func printScopes(scopes []string) {
165+
color.Yellow("[i] Scopes:")
166+
t := table.NewWriter()
167+
t.SetOutputMirror(os.Stdout)
168+
t.AppendHeader(table.Row{"Scopes"})
169+
for _, scope := range scopes {
170+
t.AppendRow(table.Row{color.GreenString(scope)})
171+
}
172+
t.Render()
173+
}
174+
175+
func printResources(resources []FastlyResource) {
176+
color.Yellow("[i] Resources:")
177+
t := table.NewWriter()
178+
t.SetOutputMirror(os.Stdout)
179+
t.AppendHeader(table.Row{"Name", "Type"})
180+
for _, resource := range resources {
181+
t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
182+
}
183+
184+
t.Render()
185+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package fastly
2+
3+
import (
4+
_ "embed"
5+
"encoding/json"
6+
"sort"
7+
"testing"
8+
"time"
9+
10+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
14+
)
15+
16+
//go:embed result_output.json
17+
var expectedOutput []byte
18+
19+
func TestAnalyzer_Analyze(t *testing.T) {
20+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
21+
defer cancel()
22+
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
23+
if err != nil {
24+
t.Fatalf("could not get test secrets from GCP: %s", err)
25+
}
26+
27+
key := testSecrets.MustGetField("FASTLYPERSONALTOKEN_TOKEN")
28+
29+
tests := []struct {
30+
name string
31+
key string
32+
want []byte // JSON string
33+
wantErr bool
34+
}{
35+
{
36+
name: "valid fastly token",
37+
key: key,
38+
want: expectedOutput,
39+
wantErr: false,
40+
},
41+
}
42+
43+
for _, tt := range tests {
44+
t.Run(tt.name, func(t *testing.T) {
45+
a := Analyzer{Cfg: &config.Config{}}
46+
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
47+
if (err != nil) != tt.wantErr {
48+
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
49+
return
50+
}
51+
52+
// Bindings need to be in the same order to be comparable
53+
sortBindings(got.Bindings)
54+
55+
// Marshal the actual result to JSON
56+
gotJSON, err := json.Marshal(got)
57+
if err != nil {
58+
t.Fatalf("could not marshal got to JSON: %s", err)
59+
}
60+
61+
// Parse the expected JSON string
62+
var wantObj analyzers.AnalyzerResult
63+
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
64+
t.Fatalf("could not unmarshal want JSON string: %s", err)
65+
}
66+
67+
// Bindings need to be in the same order to be comparable
68+
sortBindings(wantObj.Bindings)
69+
70+
// Marshal the expected result to JSON (to normalize)
71+
wantJSON, err := json.Marshal(wantObj)
72+
if err != nil {
73+
t.Fatalf("could not marshal want to JSON: %s", err)
74+
}
75+
76+
// Compare the JSON strings
77+
if string(gotJSON) != string(wantJSON) {
78+
// Pretty-print both JSON strings for easier comparison
79+
var gotIndented, wantIndented []byte
80+
gotIndented, err = json.MarshalIndent(got, "", " ")
81+
if err != nil {
82+
t.Fatalf("could not marshal got to indented JSON: %s", err)
83+
}
84+
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
85+
if err != nil {
86+
t.Fatalf("could not marshal want to indented JSON: %s", err)
87+
}
88+
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
89+
}
90+
})
91+
}
92+
}
93+
94+
// Helper function to sort bindings
95+
func sortBindings(bindings []analyzers.Binding) {
96+
sort.SliceStable(bindings, func(i, j int) bool {
97+
if bindings[i].Resource.FullyQualifiedName == bindings[j].Resource.FullyQualifiedName {
98+
return bindings[i].Permission.Value < bindings[j].Permission.Value
99+
}
100+
return bindings[i].Resource.FullyQualifiedName < bindings[j].Resource.FullyQualifiedName
101+
})
102+
}

0 commit comments

Comments
 (0)