Skip to content

Commit 9a2b268

Browse files
Added DataBricks Analyzer (#4135)
* initial commit * initial structure * initial commit * added databricks analyzer * removed print statement * resolved ahrav the legend comments * resolved comments * fixed issue
1 parent d794f9b commit 9a2b268

File tree

12 files changed

+962
-28
lines changed

12 files changed

+962
-28
lines changed

pkg/analyzer/analyzers/analyzers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ const (
101101
AnalyzerTypeMux
102102
AnalyzerTypePosthog
103103
AnalyzerTypeDropbox
104+
AnalyzerTypeDataBricks
104105
// Add new items here with AnalyzerType prefix
105106
)
106107

@@ -147,6 +148,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
147148
AnalyzerTypeMux: "Mux",
148149
AnalyzerTypePosthog: "Posthog",
149150
AnalyzerTypeDropbox: "Dropbox",
151+
AnalyzerTypeDataBricks: "DataBricks",
150152
// Add new mappings here
151153
}
152154

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
//go:generate generate_permissions permissions.yaml permissions.go databricks
2+
package databricks
3+
4+
import (
5+
"fmt"
6+
"os"
7+
8+
"github.com/fatih/color"
9+
"github.com/jedib0t/go-pretty/v6/table"
10+
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
14+
)
15+
16+
var _ analyzers.Analyzer = (*Analyzer)(nil)
17+
18+
type Analyzer struct {
19+
Cfg *config.Config
20+
}
21+
22+
func (a Analyzer) Type() analyzers.AnalyzerType {
23+
return analyzers.AnalyzerTypeDataBricks
24+
}
25+
26+
func (a Analyzer) Analyze(ctx context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
27+
token, exist := credInfo["token"]
28+
if !exist {
29+
return nil, fmt.Errorf("key not found in credential info")
30+
}
31+
32+
domain, exist := credInfo["domain"]
33+
if !exist {
34+
return nil, fmt.Errorf("domain not found in credential info")
35+
}
36+
37+
info, err := AnalyzePermissions(ctx, a.Cfg, domain, token)
38+
if err != nil {
39+
return nil, err
40+
}
41+
42+
return secretInfoToAnalyzerResult(info), nil
43+
}
44+
45+
func AnalyzeAndPrintPermissions(cfg *config.Config, domain, token string) {
46+
ctx := context.Background()
47+
48+
info, err := AnalyzePermissions(ctx, cfg, domain, token)
49+
if err != nil {
50+
// just print the error in cli and continue as a partial success
51+
color.Red("[x] Error : %s", err.Error())
52+
}
53+
54+
if info == nil {
55+
color.Red("[x] Error : %s", "No information found")
56+
return
57+
}
58+
59+
color.Green("[!] Valid DataBricks Access Token\n\n")
60+
61+
printUserInfo(info.UserInfo)
62+
printTokenInfo(info.Tokens)
63+
printPermissions(info.TokenPermissionLevels)
64+
65+
if len(info.Resources) > 0 {
66+
printResources(info.Resources)
67+
}
68+
69+
color.Yellow("\n[i] Expires: %s", "N/A (Refer to Token Information Table)")
70+
}
71+
72+
func AnalyzePermissions(ctx context.Context, cfg *config.Config, domain, token string) (*SecretInfo, error) {
73+
client := analyzers.NewAnalyzeClient(cfg)
74+
75+
var secretInfo = &SecretInfo{}
76+
77+
if err := captureUserInfo(ctx, client, domain, token, secretInfo); err != nil {
78+
return nil, err
79+
}
80+
81+
if err := captureTokensInfo(ctx, client, domain, token, secretInfo); err != nil {
82+
return secretInfo, err
83+
}
84+
85+
if err := captureTokenPermissions(ctx, client, domain, token, secretInfo); err != nil {
86+
return secretInfo, err
87+
}
88+
89+
// capture resources
90+
if err := captureDataBricksResources(ctx, client, domain, token, secretInfo); err != nil {
91+
return secretInfo, err
92+
}
93+
94+
return secretInfo, nil
95+
}
96+
97+
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
98+
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
99+
if info == nil {
100+
return nil
101+
}
102+
103+
result := analyzers.AnalyzerResult{
104+
AnalyzerType: analyzers.AnalyzerTypeDataBricks,
105+
Metadata: map[string]any{},
106+
Bindings: make([]analyzers.Binding, 0),
107+
}
108+
109+
// extract information from resource to create bindings and append to result bindings
110+
for _, resource := range info.Resources {
111+
binding := analyzers.Binding{
112+
Resource: analyzers.Resource{
113+
Name: resource.Name,
114+
FullyQualifiedName: fmt.Sprintf("databricks/%s/%s", resource.Type, resource.ID), // e.g: netlify/site/123
115+
Type: resource.Type,
116+
Metadata: map[string]any{}, // to avoid panic
117+
},
118+
}
119+
120+
for key, value := range resource.Metadata {
121+
binding.Resource.Metadata[key] = value
122+
}
123+
124+
// for each permission add a binding to resource
125+
for _, perm := range info.TokenPermissionLevels {
126+
binding.Permission = analyzers.Permission{
127+
Value: perm,
128+
}
129+
130+
result.Bindings = append(result.Bindings, binding)
131+
}
132+
}
133+
134+
return &result
135+
}
136+
137+
// cli print functions
138+
func printUserInfo(user User) {
139+
color.Yellow("[i] User Information:")
140+
t := table.NewWriter()
141+
t.SetOutputMirror(os.Stdout)
142+
t.AppendHeader(table.Row{"ID", "UserName", "Primary Email"})
143+
t.AppendRow(table.Row{color.GreenString(user.ID), color.GreenString(user.UserName), color.GreenString(user.PrimaryEmail)})
144+
145+
t.Render()
146+
}
147+
148+
func printTokenInfo(tokens []Token) {
149+
color.Yellow("[i] Tokens Information:")
150+
t := table.NewWriter()
151+
t.SetOutputMirror(os.Stdout)
152+
t.AppendHeader(table.Row{"Name", "Expiry Time", "Created By", "Last Used At"})
153+
for _, token := range tokens {
154+
t.AppendRow(table.Row{color.GreenString(token.Name),
155+
color.GreenString(token.ExpiryTime), color.GreenString(token.CreatedBy), color.GreenString(token.LastUsedDay)})
156+
}
157+
t.Render()
158+
}
159+
160+
func printPermissions(permissions []string) {
161+
color.Yellow("[i] Token Permission Levels:")
162+
t := table.NewWriter()
163+
t.SetOutputMirror(os.Stdout)
164+
t.AppendHeader(table.Row{"Permission Level"})
165+
for _, permission := range permissions {
166+
t.AppendRow(table.Row{color.GreenString(permission)})
167+
}
168+
t.Render()
169+
}
170+
171+
func printResources(resources []DataBricksResource) {
172+
color.Yellow("[i] Resources:")
173+
t := table.NewWriter()
174+
t.SetOutputMirror(os.Stdout)
175+
t.AppendHeader(table.Row{"Name", "Type"})
176+
for _, resource := range resources {
177+
t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
178+
}
179+
t.Render()
180+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package databricks
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", "analyzers1")
23+
if err != nil {
24+
t.Fatalf("could not get test secrets from GCP: %s", err)
25+
}
26+
27+
token := testSecrets.MustGetField("DATABRICKS_TOKEN")
28+
domain := testSecrets.MustGetField("DATABRICKS_DOMAIN")
29+
30+
tests := []struct {
31+
name string
32+
domain string
33+
token string
34+
want []byte // JSON string
35+
wantErr bool
36+
}{
37+
{
38+
name: "valid databricks credentials",
39+
domain: domain,
40+
token: token,
41+
want: expectedOutput,
42+
wantErr: false,
43+
},
44+
}
45+
46+
for _, tt := range tests {
47+
t.Run(tt.name, func(t *testing.T) {
48+
a := Analyzer{Cfg: &config.Config{}}
49+
got, err := a.Analyze(ctx, map[string]string{"token": tt.token, "domain": tt.domain})
50+
if (err != nil) != tt.wantErr {
51+
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
52+
return
53+
}
54+
55+
// Bindings need to be in the same order to be comparable
56+
sortBindings(got.Bindings)
57+
58+
// Marshal the actual result to JSON
59+
gotJSON, err := json.Marshal(got)
60+
if err != nil {
61+
t.Fatalf("could not marshal got to JSON: %s", err)
62+
}
63+
64+
// Parse the expected JSON string
65+
var wantObj analyzers.AnalyzerResult
66+
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
67+
t.Fatalf("could not unmarshal want JSON string: %s", err)
68+
}
69+
70+
// Bindings need to be in the same order to be comparable
71+
sortBindings(wantObj.Bindings)
72+
73+
// Marshal the expected result to JSON (to normalize)
74+
wantJSON, err := json.Marshal(wantObj)
75+
if err != nil {
76+
t.Fatalf("could not marshal want to JSON: %s", err)
77+
}
78+
79+
// Compare the JSON strings
80+
if string(gotJSON) != string(wantJSON) {
81+
// Pretty-print both JSON strings for easier comparison
82+
var gotIndented, wantIndented []byte
83+
gotIndented, err = json.MarshalIndent(got, "", " ")
84+
if err != nil {
85+
t.Fatalf("could not marshal got to indented JSON: %s", err)
86+
}
87+
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
88+
if err != nil {
89+
t.Fatalf("could not marshal want to indented JSON: %s", err)
90+
}
91+
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
92+
}
93+
})
94+
}
95+
}
96+
97+
// Helper function to sort bindings
98+
func sortBindings(bindings []analyzers.Binding) {
99+
sort.SliceStable(bindings, func(i, j int) bool {
100+
if bindings[i].Resource.Name == bindings[j].Resource.Name {
101+
return bindings[i].Permission.Value < bindings[j].Permission.Value
102+
}
103+
return bindings[i].Resource.Name < bindings[j].Resource.Name
104+
})
105+
}

0 commit comments

Comments
 (0)