Files
trufflehog/pkg/analyzer/analyzers/dockerhub/dockerhub.go
Jordan Tunstill 4ec774956c preallocate bindings capacity in dockerhub and groq, with unit tests (#5213)
* preallocate bindings capacity in dockerhub and groq, with unit tests

* ran linter
2026-08-20 14:36:24 -07:00

196 lines
5.6 KiB
Go

//go:generate generate_permissions permissions.yaml permissions.go dockerhub
package dockerhub
import (
"errors"
"os"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/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, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationValidateCredentials, analyzers.ServiceConfig, "", errors.New("username not found in the credentials info"),
)
}
pat, exist := credInfo["pat"]
if !exist {
return nil, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationValidateCredentials, analyzers.ServiceConfig, "", errors.New("personal access token(PAT) not found in the credentials info"),
)
}
info, err := AnalyzePermissions(a.Cfg, username, pat)
if err != nil {
return nil, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationAnalyzePermissions, analyzers.ServiceAPI, "", 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{}
// 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, 0, 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()
}