diff --git a/pkg/analyzer/analyzers/dockerhub/dockerhub.go b/pkg/analyzer/analyzers/dockerhub/dockerhub.go index 5584bd9e0..bc400869d 100644 --- a/pkg/analyzer/analyzers/dockerhub/dockerhub.go +++ b/pkg/analyzer/analyzers/dockerhub/dockerhub.go @@ -133,7 +133,7 @@ func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult { result := analyzers.AnalyzerResult{ AnalyzerType: analyzers.AnalyzerTypeDockerHub, Metadata: map[string]any{"Valid_Key": info.Valid}, - Bindings: make([]analyzers.Binding, len(info.Repositories)), + Bindings: make([]analyzers.Binding, 0, len(info.Repositories)), } // extract information to create bindings and append to result bindings diff --git a/pkg/analyzer/analyzers/dockerhub/dockerhub_test.go b/pkg/analyzer/analyzers/dockerhub/dockerhub_test.go index 6cc37749c..512ab6c36 100644 --- a/pkg/analyzer/analyzers/dockerhub/dockerhub_test.go +++ b/pkg/analyzer/analyzers/dockerhub/dockerhub_test.go @@ -15,6 +15,90 @@ import ( //go:embed result_output.json var expectedOutput []byte +func repo(name string) Repository { + return Repository{ + ID: "user123/repo/image/" + name, + Name: name, + Type: "image", + IsPrivate: true, + StarCount: 1, + PullCount: 2, + } +} + +func TestSecretInfoToAnalyzerResult(t *testing.T) { + tests := []struct { + name string + repos []Repository + wantLen int + wantNames []string + }{ + { + name: "one repository produces exactly one binding", + repos: []Repository{repo("repo1")}, + wantLen: 1, + wantNames: []string{"repo1"}, + }, + { + name: "three repositories produce exactly three bindings, not six", + repos: []Repository{repo("repo1"), repo("repo2"), repo("repo3")}, + wantLen: 3, + wantNames: []string{"repo1", "repo2", "repo3"}, + }, + { + name: "no repositories: no bindings", + repos: nil, + wantLen: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + info := &SecretInfo{ + Valid: true, + User: User{ID: "uuid-123", Username: "user123", Email: "user123@example.com"}, + Permissions: []string{"repo:admin"}, + Repositories: tt.repos, + } + + result := secretInfoToAnalyzerResult(info) + if result == nil { + t.Fatal("secretInfoToAnalyzerResult() returned nil") + } + + if len(result.Bindings) != tt.wantLen { + t.Errorf("got %d bindings, want %d", len(result.Bindings), tt.wantLen) + } + + // Mirrors the four proto min_len constraints so this test fails for + // the same reason the enterprise API does. + for i, binding := range result.Bindings { + if binding.Resource.Name == "" { + t.Errorf("binding[%d].Resource.Name is empty", i) + } + if binding.Resource.Type == "" { + t.Errorf("binding[%d].Resource.Type is empty", i) + } + if binding.Resource.FullyQualifiedName == "" { + t.Errorf("binding[%d].Resource.FullyQualifiedName is empty", i) + } + if binding.Permission.Value == "" { + t.Errorf("binding[%d].Permission.Value is empty", i) + } + } + + for i, name := range tt.wantNames { + if i >= len(result.Bindings) { + break + } + if result.Bindings[i].Resource.Name != name { + t.Errorf("binding[%d].Resource.Name = %q, want %q", i, result.Bindings[i].Resource.Name, name) + } + } + }) + } +} + func TestAnalyzer_Analyze(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) defer cancel() diff --git a/pkg/analyzer/analyzers/groq/groq.go b/pkg/analyzer/analyzers/groq/groq.go index 1e345cfd1..1e4f08d5d 100644 --- a/pkg/analyzer/analyzers/groq/groq.go +++ b/pkg/analyzer/analyzers/groq/groq.go @@ -121,7 +121,7 @@ func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult { result := analyzers.AnalyzerResult{ AnalyzerType: analyzers.AnalyzerTypeGroq, Metadata: map[string]any{"Valid_Key": info.Valid}, - Bindings: make([]analyzers.Binding, len(info.GroqResources)), + Bindings: make([]analyzers.Binding, 0, len(info.GroqResources)), } // extract information to create bindings and append to result bindings diff --git a/pkg/analyzer/analyzers/groq/groq_test.go b/pkg/analyzer/analyzers/groq/groq_test.go index ef73d217b..fef55a02b 100644 --- a/pkg/analyzer/analyzers/groq/groq_test.go +++ b/pkg/analyzer/analyzers/groq/groq_test.go @@ -12,6 +12,91 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/context" ) +func resource(id, name, resourceType string) GroqResource { + return GroqResource{ + ID: id, + Name: name, + Type: resourceType, + Permission: PermissionStrings[FullAccess], + Metadata: map[string]string{"status": "completed"}, + } +} + +func TestSecretInfoToAnalyzerResult(t *testing.T) { + tests := []struct { + name string + resources []GroqResource + wantLen int + wantNames []string + }{ + { + name: "one resource produces exactly one binding", + resources: []GroqResource{resource("batch_123", "batch_123", "batch")}, + wantLen: 1, + wantNames: []string{"batch_123"}, + }, + { + name: "three resources produce exactly three bindings, not six", + resources: []GroqResource{ + resource("batch_123", "batch_123", "batch"), + resource("file_456", "training.jsonl", "file"), + resource("file_789", "eval.jsonl", "file"), + }, + wantLen: 3, + wantNames: []string{"batch_123", "training.jsonl", "eval.jsonl"}, + }, + { + name: "no resources: no bindings", + resources: nil, + wantLen: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + info := &SecretInfo{ + Valid: true, + GroqResources: tt.resources, + } + + result := secretInfoToAnalyzerResult(info) + if result == nil { + t.Fatal("secretInfoToAnalyzerResult() returned nil") + } + + if len(result.Bindings) != tt.wantLen { + t.Errorf("got %d bindings, want %d", len(result.Bindings), tt.wantLen) + } + + // Mirrors the four proto min_len constraints so this test fails for + // the same reason the enterprise API does. + for i, binding := range result.Bindings { + if binding.Resource.Name == "" { + t.Errorf("binding[%d].Resource.Name is empty", i) + } + if binding.Resource.Type == "" { + t.Errorf("binding[%d].Resource.Type is empty", i) + } + if binding.Resource.FullyQualifiedName == "" { + t.Errorf("binding[%d].Resource.FullyQualifiedName is empty", i) + } + if binding.Permission.Value == "" { + t.Errorf("binding[%d].Permission.Value is empty", i) + } + } + + for i, name := range tt.wantNames { + if i >= len(result.Bindings) { + break + } + if result.Bindings[i].Resource.Name != name { + t.Errorf("binding[%d].Resource.Name = %q, want %q", i, result.Bindings[i].Resource.Name, name) + } + } + }) + } +} + func TestAnalyzer_Analyze(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) defer cancel()