Skip to content

Commit d20b3a4

Browse files
Netlify Analyzer (#4106)
* initial * added netlify analyzer
1 parent 426d08b commit d20b3a4

File tree

9 files changed

+1939
-0
lines changed

9 files changed

+1939
-0
lines changed

pkg/analyzer/analyzers/analyzers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ const (
9494
AnalyzerTypeLaunchDarkly
9595
AnalyzerTypeFigma
9696
AnalyzerTypePlaid
97+
AnalyzerTypeNetlify
9798
// Add new items here with AnalyzerType prefix
9899
)
99100

@@ -133,6 +134,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
133134
AnalyzerTypeLaunchDarkly: "LaunchDarkly",
134135
AnalyzerTypeFigma: "Figma",
135136
AnalyzerTypePlaid: "Plaid",
137+
AnalyzerTypeNetlify: "Netlify",
136138
// Add new mappings here
137139
}
138140

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package netlify
2+
3+
import "sync"
4+
5+
type ResourceType string
6+
7+
func (r ResourceType) String() string {
8+
return string(r)
9+
}
10+
11+
const (
12+
CurrentUser ResourceType = "User"
13+
Token ResourceType = "Token"
14+
Site ResourceType = "Site"
15+
SiteFile ResourceType = "Site File"
16+
SiteEnvVar ResourceType = "Site Env Variable"
17+
SiteSnippet ResourceType = "Site Snippet"
18+
SiteDeploy ResourceType = "Site Deploy"
19+
SiteDeployedBranch ResourceType = "Site Deployed Branch"
20+
SiteBuild ResourceType = "Site Build"
21+
SiteDevServer ResourceType = "Site Dev Server"
22+
SiteBuildHook ResourceType = "Site Build Hook"
23+
SiteDevServerHook ResourceType = "Site Dev Server Hook"
24+
SiteServiceInstance ResourceType = "Site Service Instance"
25+
SiteFunction ResourceType = "Site Function"
26+
SiteForm ResourceType = "Site Form"
27+
SiteSubmission ResourceType = "Site Submission"
28+
SiteTrafficSplit ResourceType = "Site Traffic Split"
29+
DNSZone ResourceType = "DNS Zone"
30+
Service ResourceType = "Service"
31+
)
32+
33+
type SecretInfo struct {
34+
mu sync.RWMutex
35+
36+
UserInfo User
37+
Resources []NetlifyResource
38+
}
39+
40+
func (s *SecretInfo) appendResource(resource NetlifyResource) {
41+
s.mu.Lock()
42+
defer s.mu.Unlock()
43+
44+
s.Resources = append(s.Resources, resource)
45+
}
46+
47+
// listResourceByType returns a list of resources matching the given type.
48+
func (s *SecretInfo) listResourceByType(resourceType ResourceType) []NetlifyResource {
49+
s.mu.RLock()
50+
defer s.mu.RUnlock()
51+
52+
resources := make([]NetlifyResource, 0, len(s.Resources))
53+
for _, resource := range s.Resources {
54+
if resource.Type == resourceType.String() {
55+
resources = append(resources, resource)
56+
}
57+
}
58+
59+
return resources
60+
}
61+
62+
type User struct {
63+
ID string `json:"id"`
64+
Name string `json:"full_name"`
65+
Email string `json:"email"`
66+
AccountID string `json:"account_id"`
67+
LastLogin string `json:"last_login"`
68+
}
69+
70+
type NetlifyResource struct {
71+
ID string
72+
Name string
73+
Type string
74+
Metadata map[string]string
75+
Parent *NetlifyResource
76+
}
77+
78+
type token struct {
79+
ID string `json:"id"`
80+
Name string `json:"name"`
81+
Personal bool `json:"personal"`
82+
ExpiresAt string `json:"expires_at"`
83+
}
84+
85+
type site struct {
86+
SiteID string `json:"site_id"`
87+
Name string `json:"name"`
88+
Url string `json:"url"`
89+
AdminUrl string `json:"admin_url"`
90+
RepoUrl string `json:"repo_url"`
91+
}
92+
93+
type file struct {
94+
ID string `json:"id"`
95+
Path string `json:"path"`
96+
MimeType string `json:"mime_type"`
97+
}
98+
99+
type envVariable struct {
100+
Key string `json:"key"`
101+
Scopes []string `json:"scopes"`
102+
Values []struct {
103+
ID string `json:"id"`
104+
Value string `json:"value"`
105+
} `json:"values"`
106+
}
107+
108+
type snippet struct {
109+
ID string `json:"id"`
110+
Title string `json:"title"`
111+
}
112+
113+
type deploy struct {
114+
ID string `json:"id"`
115+
Name string `json:"name"`
116+
BuildID string `json:"build_id"`
117+
State string `json:"state"`
118+
Url string `json:"url"`
119+
}
120+
121+
type deployedBranch struct {
122+
ID string `json:"id"`
123+
Name string `json:"name"`
124+
Slug string `json:"slug"`
125+
}
126+
127+
type build struct {
128+
ID string `json:"id"`
129+
DeployState string `json:"deploy_state"`
130+
}
131+
132+
type devServer struct {
133+
ID string `json:"id"`
134+
Title string `json:"title"`
135+
}
136+
137+
type buildHook struct {
138+
ID string `json:"id"`
139+
Title string `json:"title"`
140+
Branch string `json:"branch"`
141+
}
142+
143+
type serviceInstance struct {
144+
ID string `json:"id"`
145+
ServiceName string `json:"service_name"`
146+
Url string `json:"url"`
147+
}
148+
149+
type function struct {
150+
ID string `json:"id"`
151+
Provider string `json:"provider"`
152+
}
153+
154+
// this handle response of 3 API's
155+
type formSubmissionSplitInfo struct {
156+
ID string `json:"id"`
157+
Name string `json:"name"`
158+
}
159+
160+
type dnsZone struct {
161+
ID string `json:"id"`
162+
Name string `json:"name"`
163+
}
164+
165+
type service struct {
166+
ID string `json:"id"`
167+
Name string `json:"name"`
168+
ServicePath string `json:"service_path"`
169+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
//go:generate generate_permissions permissions.yaml permissions.go netlify
2+
package netlify
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.AnalyzerTypeNetlify
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+
info, err := AnalyzePermissions(a.Cfg, key)
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
return secretInfoToAnalyzerResult(info), nil
37+
}
38+
39+
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
40+
info, err := AnalyzePermissions(cfg, key)
41+
if err != nil {
42+
// just print the error in cli and continue as a partial success
43+
color.Red("[x] Error : %s", err.Error())
44+
}
45+
46+
if info == nil {
47+
color.Red("[x] Error : %s", "No information found")
48+
return
49+
}
50+
51+
color.Green("[!] Valid Fastly API key\n\n")
52+
53+
printUserInfo(info.UserInfo)
54+
printTokenInfo(info.listResourceByType(Token))
55+
printResources(info.Resources)
56+
57+
color.Yellow("\n[i] Expires: %s", "N/A (Refer to Token Information Table)")
58+
}
59+
60+
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
61+
client := analyzers.NewAnalyzeClient(cfg)
62+
63+
var secretInfo = &SecretInfo{}
64+
65+
if err := captureUserInfo(client, key, secretInfo); err != nil {
66+
return nil, err
67+
}
68+
69+
if err := captureTokens(client, key, secretInfo); err != nil {
70+
return nil, err
71+
}
72+
73+
if err := captureResources(client, key, secretInfo); err != nil {
74+
return secretInfo, err
75+
}
76+
77+
return secretInfo, nil
78+
}
79+
80+
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
81+
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
82+
if info == nil {
83+
return nil
84+
}
85+
86+
result := analyzers.AnalyzerResult{
87+
AnalyzerType: analyzers.AnalyzerTypeNetlify,
88+
Metadata: map[string]any{},
89+
Bindings: make([]analyzers.Binding, 0),
90+
}
91+
92+
// extract information from resource to create bindings and append to result bindings
93+
for _, resource := range info.Resources {
94+
binding := analyzers.Binding{
95+
Resource: analyzers.Resource{
96+
Name: resource.Name,
97+
FullyQualifiedName: fmt.Sprintf("netlify/%s/%s", resource.Type, resource.ID), // e.g: netlify/site/123
98+
Type: resource.Type,
99+
Metadata: map[string]any{}, // to avoid panic
100+
},
101+
Permission: analyzers.Permission{
102+
Value: PermissionStrings[FullAccess], // no fine grain access
103+
},
104+
}
105+
106+
if resource.Parent != nil {
107+
binding.Resource.Parent = &analyzers.Resource{
108+
Name: resource.Parent.Name,
109+
FullyQualifiedName: resource.Parent.ID,
110+
Type: resource.Parent.Type,
111+
// not copying parent metadata
112+
}
113+
}
114+
115+
for key, value := range resource.Metadata {
116+
binding.Resource.Metadata[key] = value
117+
}
118+
119+
result.Bindings = append(result.Bindings, binding)
120+
}
121+
122+
return &result
123+
}
124+
125+
// cli print functions
126+
func printUserInfo(user User) {
127+
color.Yellow("[i] User Information:")
128+
t := table.NewWriter()
129+
t.SetOutputMirror(os.Stdout)
130+
t.AppendHeader(table.Row{"Name", "Email", "Account ID", "Last Login At"})
131+
t.AppendRow(table.Row{color.GreenString(user.Name), color.GreenString(user.Email), color.GreenString(user.AccountID), color.GreenString(user.LastLogin)})
132+
133+
t.Render()
134+
}
135+
136+
func printTokenInfo(tokens []NetlifyResource) {
137+
color.Yellow("[i] Tokens Information:")
138+
t := table.NewWriter()
139+
t.SetOutputMirror(os.Stdout)
140+
t.AppendHeader(table.Row{"ID", "Name", "Personal", "Expires At"})
141+
for _, token := range tokens {
142+
t.AppendRow(table.Row{color.GreenString(token.ID), color.GreenString(token.Name), color.GreenString(token.Metadata[tokenPersonal]), color.GreenString(token.Metadata[tokenExpiresAt])})
143+
}
144+
t.Render()
145+
}
146+
147+
func printResources(resources []NetlifyResource) {
148+
color.Yellow("[i] Resources:")
149+
t := table.NewWriter()
150+
t.SetOutputMirror(os.Stdout)
151+
t.AppendHeader(table.Row{"Name", "Type"})
152+
for _, resource := range resources {
153+
// skip token type resource as we will print them separately
154+
if resource.Type == Token.String() {
155+
continue
156+
}
157+
158+
t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
159+
}
160+
t.Render()
161+
}

0 commit comments

Comments
 (0)