diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index 2e820cf8a5c..3bffb79984f 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -255,6 +255,15 @@ func processFile(ctx context.Context, file string, project *types.Project, extFi } for name, service := range base.Services { for i, envFile := range service.EnvFiles { + if _, statErr := os.Stat(envFile.Path); statErr != nil { + if !os.IsNotExist(statErr) { + return nil, fmt.Errorf("failed to access env file %s: %w", envFile.Path, statErr) + } + if envFile.Required { + return nil, fmt.Errorf("env file %s not found", envFile.Path) + } + continue + } hash := fmt.Sprintf("%x.env", sha256.Sum256([]byte(envFile.Path))) envFiles[envFile.Path] = hash f, err = transform.ReplaceEnvFile(f, name, i, hash) @@ -690,6 +699,15 @@ func (s *composeService) checkForSensitiveData(ctx context.Context, project *typ for _, service := range project.Services { // Check env files for _, envFile := range service.EnvFiles { + if _, statErr := os.Stat(envFile.Path); statErr != nil { + if !os.IsNotExist(statErr) { + return nil, fmt.Errorf("failed to access env file %s: %w", envFile.Path, statErr) + } + if envFile.Required { + return nil, fmt.Errorf("env file %s not found", envFile.Path) + } + continue + } findings, err := scan.ScanFile(envFile.Path) if err != nil { return nil, fmt.Errorf("failed to scan env file %s: %w", envFile.Path, err) diff --git a/pkg/compose/publish_test.go b/pkg/compose/publish_test.go index 21cdfa1bed3..ca424367725 100644 --- a/pkg/compose/publish_test.go +++ b/pkg/compose/publish_test.go @@ -136,6 +136,155 @@ func Test_preChecks_sensitive_data_detected_decline(t *testing.T) { assert.Equal(t, accept, false) } +func Test_processFile_optional_env_file_missing(t *testing.T) { + dir := t.TempDir() + composePath := filepath.Join(dir, "compose.yaml") + composeContent := `name: test +services: + web: + image: nginx + env_file: + - path: missing.env + required: false +` + assert.NilError(t, os.WriteFile(composePath, []byte(composeContent), 0o600)) + + project, err := loader.LoadWithContext(t.Context(), types.ConfigDetails{ + WorkingDir: dir, + Environment: types.Mapping{}, + ConfigFiles: []types.ConfigFile{{Filename: composePath}}, + }) + assert.NilError(t, err) + + extFiles := map[string]string{} + envFiles := map[string]string{} + _, err = processFile(t.Context(), composePath, project, extFiles, envFiles) + assert.NilError(t, err, "optional missing env file should not cause error") + assert.Equal(t, len(envFiles), 0, "missing optional env file should not be added") +} + +func Test_processFile_optional_env_file_present(t *testing.T) { + dir := t.TempDir() + envPath := filepath.Join(dir, "app.env") + assert.NilError(t, os.WriteFile(envPath, []byte("FOO=bar\n"), 0o600)) + + composePath := filepath.Join(dir, "compose.yaml") + composeContent := `name: test +services: + web: + image: nginx + env_file: + - path: app.env + required: false +` + assert.NilError(t, os.WriteFile(composePath, []byte(composeContent), 0o600)) + + project, err := loader.LoadWithContext(t.Context(), types.ConfigDetails{ + WorkingDir: dir, + Environment: types.Mapping{}, + ConfigFiles: []types.ConfigFile{{Filename: composePath}}, + }) + assert.NilError(t, err) + + extFiles := map[string]string{} + envFiles := map[string]string{} + _, err = processFile(t.Context(), composePath, project, extFiles, envFiles) + assert.NilError(t, err) + assert.Equal(t, len(envFiles), 1, "present optional env file should be added") +} + +func Test_processFile_required_env_file_missing(t *testing.T) { + dir := t.TempDir() + composePath := filepath.Join(dir, "compose.yaml") + composeContent := `name: test +services: + web: + image: nginx + env_file: + - path: missing.env + required: true +` + assert.NilError(t, os.WriteFile(composePath, []byte(composeContent), 0o600)) + + // SkipResolveEnvironment: the loader itself errors on missing required env + // files; skip that so we can test processFile's own check. + project, err := loader.LoadWithContext(t.Context(), types.ConfigDetails{ + WorkingDir: dir, + Environment: types.Mapping{}, + ConfigFiles: []types.ConfigFile{{Filename: composePath}}, + }, func(options *loader.Options) { + options.SkipResolveEnvironment = true + }) + assert.NilError(t, err) + + extFiles := map[string]string{} + envFiles := map[string]string{} + _, err = processFile(t.Context(), composePath, project, extFiles, envFiles) + assert.ErrorContains(t, err, "not found", "required missing env file should fail") +} + +func Test_checkForSensitiveData_optional_env_file_missing(t *testing.T) { + dir := t.TempDir() + project := &types.Project{ + Services: types.Services{ + "web": { + Name: "web", + Image: "nginx", + EnvFiles: []types.EnvFile{ + {Path: filepath.Join(dir, "missing.env"), Required: false}, + }, + }, + }, + } + + svc := &composeService{} + findings, err := svc.checkForSensitiveData(t.Context(), project) + assert.NilError(t, err, "optional missing env file should not cause error during scan") + assert.Equal(t, len(findings), 0) +} + +func Test_checkForSensitiveData_optional_env_file_present(t *testing.T) { + dir := t.TempDir() + envPath := filepath.Join(dir, "secrets.env") + assert.NilError(t, os.WriteFile(envPath, []byte(`AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"`), 0o600)) + + project := &types.Project{ + Services: types.Services{ + "web": { + Name: "web", + Image: "nginx", + EnvFiles: []types.EnvFile{ + {Path: envPath, Required: false}, + }, + }, + }, + } + + svc := &composeService{} + findings, err := svc.checkForSensitiveData(t.Context(), project) + assert.NilError(t, err) + assert.Assert(t, len(findings) > 0, "present optional env file should still be scanned for secrets") +} + +func Test_checkForSensitiveData_required_env_file_missing(t *testing.T) { + dir := t.TempDir() + project := &types.Project{ + Services: types.Services{ + "web": { + Name: "web", + Image: "nginx", + EnvFiles: []types.EnvFile{ + {Path: filepath.Join(dir, "missing.env"), Required: true}, + }, + }, + }, + } + + svc := &composeService{} + _, err := svc.checkForSensitiveData(t.Context(), project) + assert.ErrorContains(t, err, "not found", "required missing env file should fail") +} + // --- collectEnvCheckFindings: pure detection logic --- func loadProjectForTest(t *testing.T, files map[string]string) *types.Project {