Skip to content

Commit

Permalink
Merge branch 'main' into feat/oss-101-anthropic-analyzer
Browse files Browse the repository at this point in the history
  • Loading branch information
kashifkhan0771 authored Feb 12, 2025
2 parents 352a7dd + f2dc96e commit cdd857b
Show file tree
Hide file tree
Showing 24 changed files with 1,458 additions and 56 deletions.
16 changes: 0 additions & 16 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,22 +554,6 @@ func run(state overseer.State) {
logger.V(2).Info("exiting with code 183 because results were found")
os.Exit(183)
}

// Print results.
logger.Info("finished scanning",
"chunks", metrics.ChunksScanned,
"bytes", metrics.BytesScanned,
"verified_secrets", metrics.VerifiedSecretsFound,
"unverified_secrets", metrics.UnverifiedSecretsFound,
"scan_duration", metrics.ScanDuration.String(),
"trufflehog_version", version.BuildVersion,
)

if metrics.hasFoundResults && *fail {
logger.V(2).Info("exiting with code 183 because results were found")
os.Exit(183)
}

}

func compareScans(ctx context.Context, cmd string, cfg engine.Config) error {
Expand Down
4 changes: 4 additions & 0 deletions pkg/analyzer/analyzers/analyzers.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const (
AnalyzerAnthropic
AnalyzerTypeAsana
AnalyzerTypeBitbucket
AnalyzerTypeDockerHub
AnalyzerTypeGitHub
AnalyzerTypeGitLab
AnalyzerTypeHuggingFace
Expand All @@ -82,6 +83,7 @@ const (
AnalyzerTypeStripe
AnalyzerTypeTwilio
AnalyzerTypePrivateKey
AnalyzerTypeNotion
// Add new items here with AnalyzerType prefix
)

Expand All @@ -92,6 +94,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
AnalyzerAnthropic: "Anthropic",
AnalyzerTypeAsana: "Asana",
AnalyzerTypeBitbucket: "Bitbucket",
AnalyzerTypeDockerHub: "DockerHub",
AnalyzerTypeGitHub: "GitHub",
AnalyzerTypeGitLab: "GitLab",
AnalyzerTypeHuggingFace: "HuggingFace",
Expand All @@ -110,6 +113,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
AnalyzerTypeStripe: "Stripe",
AnalyzerTypeTwilio: "Twilio",
AnalyzerTypePrivateKey: "PrivateKey",
AnalyzerTypeNotion: "Notion",
// Add new mappings here
}

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{}

// 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

0 comments on commit cdd857b

Please sign in to comment.