Skip to content

Commit 4f35c9b

Browse files
committed
Merge branch 'main' into posthog-analyzer-impl
* main: [Feat] Added Mux API Analyzer (trufflesecurity#4128) fixed name of netlify analyzer in cli output (trufflesecurity#4140) fix(discordwebhook): Update Discord webhook detector to support 19-digit IDs (trufflesecurity#4133) [Feat] Added New AccuWeather Detector Version (trufflesecurity#4114) [Feat] Added Ngrok API Key Analyzer (trufflesecurity#4110) Improved JDBC Detector Regex (trufflesecurity#4109) [Feat] Detector implementation for Azure Configuration Connection String Key (trufflesecurity#3939) test(sources/s3): fix missing region error (trufflesecurity#4131) feat(sources/s3): migrate to AWS SDK v2 (trufflesecurity#4069) Update PreCommit.md (trufflesecurity#4112) Exclusion of FalsePositive GH's usernames in PrivateKeyDetector (trufflesecurity#4046) Monday App Analyzer (trufflesecurity#4120) [Feat] Detector implementation for Azure API Management Direct Management Key (trufflesecurity#3938) Fastly Analyzer (trufflesecurity#4082) Postman Code Uses Consistent Casing for Id Var Names (trufflesecurity#4124) Normalize UID to Uid in Postman Code (trufflesecurity#4125) postman_client.IDNameUUID becomes IdNameUid (trufflesecurity#4123) Fixed Kontent Detector (trufflesecurity#4122) # Conflicts: # pkg/analyzer/analyzers/analyzers.go # pkg/analyzer/cli.go
2 parents 377efa9 + 8da5815 commit 4f35c9b

File tree

74 files changed

+6021
-412
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+6021
-412
lines changed

PreCommit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ repos:
9797
description: Detect secrets in your data.
9898
entry: bash -c 'trufflehog git file://. --since-commit HEAD --results=verified,unknown --fail'
9999
language: system
100-
stages: ["commit", "push"]
100+
stages: ["pre-commit", "pre-push"]
101101
```
102102
103103
2. Install the pre-commit hook:

go.mod

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ require (
1919
github.com/adrg/strutil v0.3.1
2020
github.com/alecthomas/kingpin/v2 v2.4.0
2121
github.com/avast/apkparser v0.0.0-20250307094510-e2100ee9c0f5
22-
github.com/aws/aws-sdk-go v1.55.6
2322
github.com/aws/aws-sdk-go-v2 v1.36.3
2423
github.com/aws/aws-sdk-go-v2/config v1.29.14
2524
github.com/aws/aws-sdk-go-v2/credentials v1.17.67
25+
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.75
26+
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3
2627
github.com/aws/aws-sdk-go-v2/service/sns v1.34.4
2728
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19
2829
github.com/aws/smithy-go v1.22.3
@@ -150,12 +151,16 @@ require (
150151
github.com/andybalholm/brotli v1.1.1 // indirect
151152
github.com/apache/arrow/go/v14 v14.0.2 // indirect
152153
github.com/atotto/clipboard v0.1.4 // indirect
154+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
153155
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
154156
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
155157
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
156158
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
159+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
157160
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
161+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1 // indirect
158162
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
163+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
159164
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
160165
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
161166
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
@@ -231,7 +236,6 @@ require (
231236
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
232237
github.com/hashicorp/go-multierror v1.1.1 // indirect
233238
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
234-
github.com/jmespath/go-jmespath v0.4.0 // indirect
235239
github.com/jpillora/s3 v1.1.4 // indirect
236240
github.com/kevinburke/ssh_config v1.2.0 // indirect
237241
github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d // indirect

go.sum

Lines changed: 15 additions & 141 deletions
Large diffs are not rendered by default.

pkg/analyzer/analyzers/analyzers.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ const (
9595
AnalyzerTypeFigma
9696
AnalyzerTypePlaid
9797
AnalyzerTypeNetlify
98+
AnalyzerTypeFastly
99+
AnalyzerTypeMonday
100+
AnalyzerTypeNgrok
101+
AnalyzerTypeMux
98102
AnalyzerTypePosthog
99103
// Add new items here with AnalyzerType prefix
100104
)
@@ -136,6 +140,10 @@ var analyzerTypeStrings = map[AnalyzerType]string{
136140
AnalyzerTypeFigma: "Figma",
137141
AnalyzerTypePlaid: "Plaid",
138142
AnalyzerTypeNetlify: "Netlify",
143+
AnalyzerTypeFastly: "Fastly",
144+
AnalyzerTypeMonday: "Monday",
145+
AnalyzerTypeNgrok: "Ngrok",
146+
AnalyzerTypeMux: "Mux",
139147
AnalyzerTypePosthog: "Posthog",
140148
// Add new mappings here
141149
}
Lines changed: 185 additions & 0 deletions
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+
}
Lines changed: 102 additions & 0 deletions
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)