Skip to content

Commit b10342e

Browse files
dockerhub analyzer (#3861)
* dockerhub analyzer * fixed linter * added test cases * resolved comments
1 parent d5f9242 commit b10342e

13 files changed

+702
-0
lines changed

pkg/analyzer/analyzers/analyzers.go

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const (
6363
AnalyzerTypeAirbrake
6464
AnalyzerTypeAsana
6565
AnalyzerTypeBitbucket
66+
AnalyzerTypeDockerHub
6667
AnalyzerTypeGitHub
6768
AnalyzerTypeGitLab
6869
AnalyzerTypeHuggingFace
@@ -90,6 +91,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
9091
AnalyzerTypeAirbrake: "Airbrake",
9192
AnalyzerTypeAsana: "Asana",
9293
AnalyzerTypeBitbucket: "Bitbucket",
94+
AnalyzerTypeDockerHub: "DockerHub",
9395
AnalyzerTypeGitHub: "GitHub",
9496
AnalyzerTypeGitLab: "GitLab",
9597
AnalyzerTypeHuggingFace: "HuggingFace",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
//go:generate generate_permissions permissions.yaml permissions.go elevenlabs
2+
package dockerhub
3+
4+
import (
5+
"errors"
6+
"os"
7+
8+
"github.com/fatih/color"
9+
"github.com/jedib0t/go-pretty/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+
// SecretInfo hold the information about the token generated from username and pat
23+
type SecretInfo struct {
24+
User User
25+
Valid bool
26+
Reference string
27+
Permissions []string
28+
Repositories []Repository
29+
ExpiresIn string
30+
Misc map[string]string
31+
}
32+
33+
// User hold the information about user to whom the personal access token belongs
34+
type User struct {
35+
ID string
36+
Username string
37+
Email string
38+
}
39+
40+
// Repository hold information about each repository the user can access
41+
type Repository struct {
42+
ID string
43+
Name string
44+
Type string
45+
IsPrivate bool
46+
StarCount int
47+
PullCount int
48+
}
49+
50+
func (a Analyzer) Type() analyzers.AnalyzerType {
51+
return analyzers.AnalyzerTypeDockerHub
52+
}
53+
54+
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
55+
username, exist := credInfo["username"]
56+
if !exist {
57+
return nil, errors.New("username not found in the credentials info")
58+
}
59+
60+
pat, exist := credInfo["pat"]
61+
if !exist {
62+
return nil, errors.New("personal access token(PAT) not found in the credentials info")
63+
}
64+
65+
info, err := AnalyzePermissions(a.Cfg, username, pat)
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
return secretInfoToAnalyzerResult(info), nil
71+
}
72+
73+
// AnalyzePermissions will collect all the scopes assigned to token along with resource it can access
74+
func AnalyzePermissions(cfg *config.Config, username, pat string) (*SecretInfo, error) {
75+
// create the http client
76+
client := analyzers.NewAnalyzeClientUnrestricted(cfg) // `/user/login` is a non-safe request
77+
78+
var secretInfo = &SecretInfo{}
79+
80+
// try to login and get jwt token
81+
token, err := login(client, username, pat)
82+
if err != nil {
83+
return nil, err
84+
}
85+
86+
if err := decodeTokenToSecretInfo(token, secretInfo); err != nil {
87+
return nil, err
88+
}
89+
90+
// fetch repositories using the jwt token and translate them to secret info
91+
if err := fetchRepositories(client, username, token, secretInfo); err != nil {
92+
return nil, err
93+
}
94+
95+
// return secret info
96+
return secretInfo, nil
97+
}
98+
99+
func AnalyzeAndPrintPermissions(cfg *config.Config, username, pat string) {
100+
info, err := AnalyzePermissions(cfg, username, pat)
101+
if err != nil {
102+
// just print the error in cli and continue as a partial success
103+
color.Red("[x] Error : %s", err.Error())
104+
}
105+
106+
if info == nil {
107+
color.Red("[x] Error : %s", "No information found")
108+
return
109+
}
110+
111+
if info.Valid {
112+
color.Green("[!] Valid DockerHub Credentials\n\n")
113+
// print user information
114+
printUser(info.User)
115+
// print permissions
116+
printPermissions(info.Permissions)
117+
// print repositories
118+
printRepositories(info.Repositories)
119+
120+
color.Yellow("\n[i] Expires: %s", info.ExpiresIn)
121+
}
122+
}
123+
124+
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
125+
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
126+
if info == nil {
127+
return nil
128+
}
129+
130+
result := analyzers.AnalyzerResult{
131+
AnalyzerType: analyzers.AnalyzerTypeDockerHub,
132+
Metadata: map[string]any{"Valid_Key": info.Valid},
133+
Bindings: make([]analyzers.Binding, len(info.Repositories)),
134+
}
135+
136+
// extract information to create bindings and append to result bindings
137+
for _, repo := range info.Repositories {
138+
binding := analyzers.Binding{
139+
Resource: analyzers.Resource{
140+
Name: repo.Name,
141+
FullyQualifiedName: repo.ID,
142+
Type: repo.Type,
143+
Metadata: map[string]any{
144+
"is_private": repo.IsPrivate,
145+
"pull_count": repo.PullCount,
146+
"star_count": repo.StarCount,
147+
},
148+
},
149+
Permission: analyzers.Permission{
150+
// as all permissions are against repo, we assign the highest available permission
151+
Value: assignHighestPermission(info.Permissions),
152+
},
153+
}
154+
155+
result.Bindings = append(result.Bindings, binding)
156+
}
157+
158+
return &result
159+
}
160+
161+
// cli print functions
162+
func printUser(user User) {
163+
color.Green("\n[i] User:")
164+
t := table.NewWriter()
165+
t.SetOutputMirror(os.Stdout)
166+
t.AppendHeader(table.Row{"ID", "Username", "Email"})
167+
t.AppendRow(table.Row{color.GreenString(user.ID), color.GreenString(user.Username), color.GreenString(user.Email)})
168+
t.Render()
169+
}
170+
171+
func printPermissions(permissions []string) {
172+
color.Yellow("[i] Permissions:")
173+
t := table.NewWriter()
174+
t.SetOutputMirror(os.Stdout)
175+
t.AppendHeader(table.Row{"Permission"})
176+
for _, permission := range permissions {
177+
t.AppendRow(table.Row{color.GreenString(permission)})
178+
}
179+
t.Render()
180+
}
181+
182+
func printRepositories(repos []Repository) {
183+
color.Green("\n[i] Repositories:")
184+
t := table.NewWriter()
185+
t.SetOutputMirror(os.Stdout)
186+
t.AppendHeader(table.Row{"Type", "ID(username/repo/repo_type/repo_name)", "Name", "Is Private", "Pull Count", "Star Count"})
187+
for _, repo := range repos {
188+
t.AppendRow(table.Row{color.GreenString(repo.Type), color.GreenString(repo.ID), color.GreenString(repo.Name),
189+
color.GreenString("%t", repo.IsPrivate), color.GreenString("%d", repo.PullCount), color.GreenString("%d", repo.StarCount)})
190+
}
191+
t.Render()
192+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package dockerhub
2+
3+
import (
4+
_ "embed"
5+
"encoding/json"
6+
"testing"
7+
"time"
8+
9+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
10+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
13+
)
14+
15+
//go:embed result_output.json
16+
var expectedOutput []byte
17+
18+
func TestAnalyzer_Analyze(t *testing.T) {
19+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
20+
defer cancel()
21+
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
22+
if err != nil {
23+
t.Fatalf("could not get test secrets from GCP: %s", err)
24+
}
25+
26+
username := testSecrets.MustGetField("DOCKERHUB_USERNAME")
27+
pat := testSecrets.MustGetField("DOCKERHUB_PAT")
28+
29+
tests := []struct {
30+
name string
31+
username string
32+
pat string
33+
want []byte // JSON string
34+
wantErr bool
35+
}{
36+
{
37+
name: "valid dockerhub credentials",
38+
username: username,
39+
pat: pat,
40+
want: expectedOutput,
41+
wantErr: false,
42+
},
43+
}
44+
45+
for _, tt := range tests {
46+
t.Run(tt.name, func(t *testing.T) {
47+
a := Analyzer{Cfg: &config.Config{}}
48+
got, err := a.Analyze(ctx, map[string]string{"username": tt.username, "pat": tt.pat})
49+
if (err != nil) != tt.wantErr {
50+
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
51+
return
52+
}
53+
54+
// Marshal the actual result to JSON
55+
gotJSON, err := json.Marshal(got)
56+
if err != nil {
57+
t.Fatalf("could not marshal got to JSON: %s", err)
58+
}
59+
60+
// Parse the expected JSON string
61+
var wantObj analyzers.AnalyzerResult
62+
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
63+
t.Fatalf("could not unmarshal want JSON string: %s", err)
64+
}
65+
66+
// Marshal the expected result to JSON (to normalize)
67+
wantJSON, err := json.Marshal(wantObj)
68+
if err != nil {
69+
t.Fatalf("could not marshal want to JSON: %s", err)
70+
}
71+
72+
// Compare the JSON strings
73+
if string(gotJSON) != string(wantJSON) {
74+
// Pretty-print both JSON strings for easier comparison
75+
var gotIndented, wantIndented []byte
76+
gotIndented, err = json.MarshalIndent(got, "", " ")
77+
if err != nil {
78+
t.Fatalf("could not marshal got to indented JSON: %s", err)
79+
}
80+
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
81+
if err != nil {
82+
t.Fatalf("could not marshal want to indented JSON: %s", err)
83+
}
84+
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
85+
}
86+
})
87+
}
88+
}

0 commit comments

Comments
 (0)