Skip to content

Commit 216a8b2

Browse files
authored
fix: critical security bug in manual mode project selection (#2009)
* fix: critical security bug in manual mode project selection
1 parent 4a7ae74 commit 216a8b2

File tree

2 files changed

+238
-8
lines changed

2 files changed

+238
-8
lines changed

cli/pkg/github/github.go

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ package github
33
import (
44
"errors"
55
"fmt"
6+
"log/slog"
7+
"os"
8+
"strings"
9+
610
"github.com/diggerhq/digger/cli/pkg/digger"
711
"github.com/diggerhq/digger/cli/pkg/drift"
812
github_models "github.com/diggerhq/digger/cli/pkg/github/models"
@@ -21,9 +25,6 @@ import (
2125
"github.com/google/go-github/v61/github"
2226
"github.com/samber/lo"
2327
"gopkg.in/yaml.v3"
24-
"log/slog"
25-
"os"
26-
"strings"
2728
)
2829

2930
func initLogger() {
@@ -145,13 +146,25 @@ func GitHubCI(lock core_locking.Lock, policyCheckerProvider core_policy.PolicyCh
145146
usage.ReportErrorAndExit(githubActor, "provide 'project' to run in 'manual' mode", 2)
146147
}
147148

148-
var projectConfig digger_config.Project
149-
for _, projectConfig = range diggerConfig.Projects {
150-
if projectConfig.Name == project {
151-
break
149+
projectConfig, projectFound := findProjectInConfig(diggerConfig.Projects, project)
150+
151+
if !projectFound {
152+
// Log available projects to help with debugging
153+
var availableProjects []string
154+
for _, p := range diggerConfig.Projects {
155+
availableProjects = append(availableProjects, p.Name)
152156
}
157+
slog.Error("Project not found in digger configuration",
158+
"requestedProject", project,
159+
"availableProjects", availableProjects)
160+
usage.ReportErrorAndExit(githubActor, fmt.Sprintf("Project '%s' not found in digger configuration. Available projects: %v", project, availableProjects), 1)
161+
}
162+
163+
slog.Info("Found project configuration", "projectName", projectConfig.Name, "projectDir", projectConfig.Dir)
164+
workflow, ok := diggerConfig.Workflows[projectConfig.Workflow]
165+
if !ok {
166+
usage.ReportErrorAndExit(githubActor, fmt.Sprintf("Workflow '%s' not found for project '%s'", projectConfig.Workflow, projectConfig.Name), 1)
153167
}
154-
workflow := diggerConfig.Workflows[projectConfig.Workflow]
155168

156169
stateEnvVars, commandEnvVars := digger_config.CollectTerraformEnvConfig(workflow.EnvVars, true)
157170

@@ -350,6 +363,16 @@ func GitHubCI(lock core_locking.Lock, policyCheckerProvider core_policy.PolicyCh
350363
usage.ReportErrorAndExit(githubActor, "Digger finished successfully", 0)
351364
}
352365

366+
// Helper function to search for a project in the configuration
367+
func findProjectInConfig(projects []digger_config.Project, projectName string) (digger_config.Project, bool) {
368+
for _, config := range projects {
369+
if config.Name == projectName {
370+
return config, true
371+
}
372+
}
373+
return digger_config.Project{}, false
374+
}
375+
353376
func logCommands(projectCommands []scheduler.Job) {
354377
logMessage := fmt.Sprintf("Following commands are going to be executed:\n")
355378
for _, pc := range projectCommands {
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package github
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/diggerhq/digger/libs/digger_config"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
// Helper function to get available project names
12+
func getAvailableProjectNames(projects []digger_config.Project) []string {
13+
var availableProjects []string
14+
for _, p := range projects {
15+
availableProjects = append(availableProjects, p.Name)
16+
}
17+
return availableProjects
18+
}
19+
20+
func TestManualModeProjectValidation(t *testing.T) {
21+
// Create a test digger config with some projects
22+
diggerConfig := &digger_config.DiggerConfig{
23+
Projects: []digger_config.Project{
24+
{
25+
Name: "project-a",
26+
Dir: "./project-a",
27+
},
28+
{
29+
Name: "project-b",
30+
Dir: "./project-b",
31+
},
32+
{
33+
Name: "project-c",
34+
Dir: "./project-c",
35+
},
36+
},
37+
}
38+
39+
tests := []struct {
40+
name string
41+
requestedProject string
42+
shouldFind bool
43+
expectedProject string
44+
}{
45+
{
46+
name: "Valid project should be found",
47+
requestedProject: "project-b",
48+
shouldFind: true,
49+
expectedProject: "project-b",
50+
},
51+
{
52+
name: "Non-existent project should not be found",
53+
requestedProject: "non-existent-project",
54+
shouldFind: false,
55+
expectedProject: "",
56+
},
57+
{
58+
name: "Empty project name should not be found",
59+
requestedProject: "",
60+
shouldFind: false,
61+
expectedProject: "",
62+
},
63+
{
64+
name: "Case sensitive project name should not be found",
65+
requestedProject: "Project-A",
66+
shouldFind: false,
67+
expectedProject: "",
68+
},
69+
}
70+
71+
for _, tt := range tests {
72+
t.Run(tt.name, func(t *testing.T) {
73+
// Use helper function for project search
74+
projectConfig, projectFound := findProjectInConfig(diggerConfig.Projects, tt.requestedProject)
75+
76+
if tt.shouldFind {
77+
assert.True(t, projectFound, "Expected to find project %s", tt.requestedProject)
78+
assert.Equal(t, tt.expectedProject, projectConfig.Name, "Expected project name to match")
79+
assert.Equal(t, "./"+tt.expectedProject, projectConfig.Dir, "Expected project directory to match")
80+
} else {
81+
assert.False(t, projectFound, "Expected NOT to find project %s", tt.requestedProject)
82+
// In the old buggy code, projectConfig would contain the last project from the loop
83+
// With our fix, projectFound will be false and we should exit with an error
84+
}
85+
})
86+
}
87+
}
88+
89+
func TestManualModeProjectValidationWithOldBuggyLogic(t *testing.T) {
90+
// This test demonstrates the old buggy behavior
91+
diggerConfig := &digger_config.DiggerConfig{
92+
Projects: []digger_config.Project{
93+
{
94+
Name: "project-a",
95+
Dir: "./project-a",
96+
},
97+
{
98+
Name: "project-b",
99+
Dir: "./project-b",
100+
},
101+
{
102+
Name: "dangerous-project",
103+
Dir: "./dangerous-project",
104+
},
105+
},
106+
}
107+
108+
// Simulate the OLD buggy logic
109+
requestedProject := "non-existent-project"
110+
var projectConfig digger_config.Project
111+
112+
// This is the OLD BUGGY CODE that would cause the issue
113+
for _, projectConfig = range diggerConfig.Projects {
114+
if projectConfig.Name == requestedProject {
115+
break
116+
}
117+
}
118+
119+
// In the old code, projectConfig would now contain "dangerous-project"
120+
// (the last project in the loop) even though we requested "non-existent-project"
121+
assert.Equal(t, "dangerous-project", projectConfig.Name,
122+
"This demonstrates the old bug: projectConfig contains the last project from the loop")
123+
assert.NotEqual(t, requestedProject, projectConfig.Name,
124+
"This shows the dangerous behavior: we're using a different project than requested")
125+
}
126+
127+
func TestAvailableProjectsLogging(t *testing.T) {
128+
// Test that we properly log available projects when a project is not found
129+
diggerConfig := &digger_config.DiggerConfig{
130+
Projects: []digger_config.Project{
131+
{Name: "web-app"},
132+
{Name: "api-service"},
133+
{Name: "database"},
134+
},
135+
}
136+
137+
requestedProject := "missing-project"
138+
139+
// Use helper functions
140+
_, projectFound := findProjectInConfig(diggerConfig.Projects, requestedProject)
141+
availableProjects := getAvailableProjectNames(diggerConfig.Projects)
142+
143+
assert.False(t, projectFound)
144+
assert.Equal(t, []string{"web-app", "api-service", "database"}, availableProjects)
145+
}
146+
147+
// This test would actually call usage.ReportErrorAndExit in a real scenario
148+
// but we can't easily test that without mocking the exit behavior
149+
func TestProjectNotFoundErrorMessage(t *testing.T) {
150+
diggerConfig := &digger_config.DiggerConfig{
151+
Projects: []digger_config.Project{
152+
{Name: "project-1"},
153+
{Name: "project-2"},
154+
},
155+
}
156+
157+
requestedProject := "invalid-project"
158+
159+
// Use helper functions
160+
_, projectFound := findProjectInConfig(diggerConfig.Projects, requestedProject)
161+
availableProjects := getAvailableProjectNames(diggerConfig.Projects)
162+
163+
assert.False(t, projectFound)
164+
165+
if !projectFound {
166+
// This would normally call usage.ReportErrorAndExit
167+
expectedErrorMsg := "Project 'invalid-project' not found in digger configuration. Available projects: [project-1 project-2]"
168+
169+
// Verify the error message format using fmt.Sprintf for better maintainability
170+
actualErrorMsg := fmt.Sprintf("Project '%s' not found in digger configuration. Available projects: %v", requestedProject, availableProjects)
171+
assert.Equal(t, expectedErrorMsg, actualErrorMsg)
172+
assert.Equal(t, []string{"project-1", "project-2"}, availableProjects)
173+
}
174+
}
175+
176+
func TestWorkflowValidation(t *testing.T) {
177+
// Test that we properly validate workflow existence
178+
diggerConfig := &digger_config.DiggerConfig{
179+
Projects: []digger_config.Project{
180+
{
181+
Name: "test-project",
182+
Workflow: "custom-workflow",
183+
},
184+
},
185+
Workflows: map[string]digger_config.Workflow{
186+
"default": {
187+
Plan: &digger_config.Stage{Steps: []digger_config.Step{{Action: "init"}}},
188+
Apply: &digger_config.Stage{Steps: []digger_config.Step{{Action: "apply"}}},
189+
},
190+
},
191+
}
192+
193+
// Test invalid workflow
194+
project := diggerConfig.Projects[0]
195+
_, workflowExists := diggerConfig.Workflows[project.Workflow]
196+
assert.False(t, workflowExists, "custom-workflow should not exist")
197+
198+
// Test that the error message is correctly formatted
199+
expectedErrorMsg := fmt.Sprintf("Workflow '%s' not found for project '%s'", project.Workflow, project.Name)
200+
actualErrorMsg := fmt.Sprintf("Workflow '%s' not found for project '%s'", project.Workflow, project.Name)
201+
assert.Equal(t, expectedErrorMsg, actualErrorMsg)
202+
203+
// Test workflow that exists
204+
project.Workflow = "default"
205+
_, workflowExists = diggerConfig.Workflows[project.Workflow]
206+
assert.True(t, workflowExists, "default workflow should exist")
207+
}

0 commit comments

Comments
 (0)