Skip to content

dockerhub analyzer #3861

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkg/analyzer/analyzers/analyzers.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const (
AnalyzerTypeAirbrake
AnalyzerTypeAsana
AnalyzerTypeBitbucket
AnalyzerTypeDockerHub
AnalyzerTypeGitHub
AnalyzerTypeGitLab
AnalyzerTypeHuggingFace
Expand Down Expand Up @@ -90,6 +91,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
AnalyzerTypeAirbrake: "Airbrake",
AnalyzerTypeAsana: "Asana",
AnalyzerTypeBitbucket: "Bitbucket",
AnalyzerTypeDockerHub: "DockerHub",
AnalyzerTypeGitHub: "GitHub",
AnalyzerTypeGitLab: "GitLab",
AnalyzerTypeHuggingFace: "HuggingFace",
Expand Down
192 changes: 192 additions & 0 deletions pkg/analyzer/analyzers/dockerhub/dockerhub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
//go:generate generate_permissions permissions.yaml permissions.go elevenlabs
package dockerhub

import (
"errors"
"os"

"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"

"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)

var _ analyzers.Analyzer = (*Analyzer)(nil)

type Analyzer struct {
Cfg *config.Config
}

// SecretInfo hold the information about the token generated from username and pat
type SecretInfo struct {
User User
Valid bool
Reference string
Permissions []string
Repositories []Repository
ExpiresIn string
Misc map[string]string
}

// User hold the information about user to whom the personal access token belongs
type User struct {
ID string
Username string
Email string
}

// Repository hold information about each repository the user can access
type Repository struct {
ID string
Name string
Type string
IsPrivate bool
StarCount int
PullCount int
}

func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeDockerHub
}

func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
username, exist := credInfo["username"]
if !exist {
return nil, errors.New("username not found in the credentials info")
}

pat, exist := credInfo["pat"]
if !exist {
return nil, errors.New("personal access token(PAT) not found in the credentials info")
}

info, err := AnalyzePermissions(a.Cfg, username, pat)
if err != nil {
return nil, err
}

return secretInfoToAnalyzerResult(info), nil
}

// AnalyzePermissions will collect all the scopes assigned to token along with resource it can access
func AnalyzePermissions(cfg *config.Config, username, pat string) (*SecretInfo, error) {
// create the http client
client := analyzers.NewAnalyzeClientUnrestricted(cfg) // `/user/login` is a non-safe request

var secretInfo = &SecretInfo{}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: This is purely a personal preference, so feel free to disregard. When passing a pointer to functions responsible for populating a struct, I find it clearer to declare the variable without &. This forces the reviewer to recognize that the pointer is being passed explicitly, indicating that we're modifying a pre-existing struct. Again, this is entirely optional.

Ex:

var secretInfo SecretInfo
...
if err := decodeTokenToSecretInfo(token, &secretInfo); err != nil {
  ...
}

if err := fetchRepositories(client, username, token, &secretInfo); err != nil {
  ...
}


// try to login and get jwt token
token, err := login(client, username, pat)
if err != nil {
return nil, err
}

if err := decodeTokenToSecretInfo(token, secretInfo); err != nil {
return nil, err
}

// fetch repositories using the jwt token and translate them to secret info
if err := fetchRepositories(client, username, token, secretInfo); err != nil {
return nil, err
}

// return secret info
return secretInfo, nil
}

func AnalyzeAndPrintPermissions(cfg *config.Config, username, pat string) {
info, err := AnalyzePermissions(cfg, username, pat)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Error : %s", err.Error())
}

if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}

if info.Valid {
color.Green("[!] Valid DockerHub Credentials\n\n")
// print user information
printUser(info.User)
// print permissions
printPermissions(info.Permissions)
// print repositories
printRepositories(info.Repositories)

color.Yellow("\n[i] Expires: %s", info.ExpiresIn)
}
}

// secretInfoToAnalyzerResult translate secret info to Analyzer Result
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}

result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeDockerHub,
Metadata: map[string]any{"Valid_Key": info.Valid},
Bindings: make([]analyzers.Binding, len(info.Repositories)),
}

// extract information to create bindings and append to result bindings
for _, repo := range info.Repositories {
binding := analyzers.Binding{
Resource: analyzers.Resource{
Name: repo.Name,
FullyQualifiedName: repo.ID,
Type: repo.Type,
Metadata: map[string]any{
"is_private": repo.IsPrivate,
"pull_count": repo.PullCount,
"star_count": repo.StarCount,
},
},
Permission: analyzers.Permission{
// as all permissions are against repo, we assign the highest available permission
Value: assignHighestPermission(info.Permissions),
},
}

result.Bindings = append(result.Bindings, binding)
}

return &result
}

// cli print functions
func printUser(user User) {
color.Green("\n[i] User:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"ID", "Username", "Email"})
t.AppendRow(table.Row{color.GreenString(user.ID), color.GreenString(user.Username), color.GreenString(user.Email)})
t.Render()
}

func printPermissions(permissions []string) {
color.Yellow("[i] Permissions:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Permission"})
for _, permission := range permissions {
t.AppendRow(table.Row{color.GreenString(permission)})
}
t.Render()
}

func printRepositories(repos []Repository) {
color.Green("\n[i] Repositories:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Type", "ID(username/repo/repo_type/repo_name)", "Name", "Is Private", "Pull Count", "Star Count"})
for _, repo := range repos {
t.AppendRow(table.Row{color.GreenString(repo.Type), color.GreenString(repo.ID), color.GreenString(repo.Name),
color.GreenString("%t", repo.IsPrivate), color.GreenString("%d", repo.PullCount), color.GreenString("%d", repo.StarCount)})
}
t.Render()
}
88 changes: 88 additions & 0 deletions pkg/analyzer/analyzers/dockerhub/dockerhub_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package dockerhub

import (
_ "embed"
"encoding/json"
"testing"
"time"

"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)

//go:embed result_output.json
var expectedOutput []byte

func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}

username := testSecrets.MustGetField("DOCKERHUB_USERNAME")
pat := testSecrets.MustGetField("DOCKERHUB_PAT")

tests := []struct {
name string
username string
pat string
want []byte // JSON string
wantErr bool
}{
{
name: "valid dockerhub credentials",
username: username,
pat: pat,
want: expectedOutput,
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"username": tt.username, "pat": tt.pat})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}

// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}

// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}

// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}

// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
})
}
}
Loading
Loading