mirror of
https://github.com/docker/compose.git
synced 2026-06-28 04:03:48 +00:00
fix(publish): honor env_file required: false for missing files
Signed-off-by: Ijtihed Kilani <ijtihedk@gmail.com>
This commit is contained in:
parent
43922d55b0
commit
9cd844243f
2 changed files with 154 additions and 1 deletions
|
|
@ -255,8 +255,21 @@ func processFile(ctx context.Context, file string, project *types.Project, extFi
|
|||
}
|
||||
for name, service := range base.Services {
|
||||
for i, envFile := range service.EnvFiles {
|
||||
// A real stat failure (e.g. permissions) is fatal, but a missing file is not:
|
||||
// the project loader already rejects missing required env files before we get
|
||||
// here, so an absent file at this point is an optional one.
|
||||
_, statErr := os.Stat(envFile.Path)
|
||||
if statErr != nil && !os.IsNotExist(statErr) {
|
||||
return nil, fmt.Errorf("failed to access env file %s: %w", envFile.Path, statErr)
|
||||
}
|
||||
// The hash is derived from the path string alone, so the env_file is always
|
||||
// rewritten to its opaque <hash>.env placeholder, even for a missing optional
|
||||
// file, so the published artifact never leaks the local path. Only files that
|
||||
// exist are registered for upload, mirroring the extends handling below.
|
||||
hash := fmt.Sprintf("%x.env", sha256.Sum256([]byte(envFile.Path)))
|
||||
envFiles[envFile.Path] = hash
|
||||
if statErr == nil {
|
||||
envFiles[envFile.Path] = hash
|
||||
}
|
||||
f, err = transform.ReplaceEnvFile(f, name, i, hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -690,6 +703,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)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@
|
|||
package compose
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
|
@ -136,6 +138,135 @@ 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{}
|
||||
data, err := processFile(t.Context(), composePath, project, extFiles, envFiles)
|
||||
assert.NilError(t, err, "optional missing env file should not cause error")
|
||||
|
||||
// The file is absent so nothing is registered for upload, but its path is still
|
||||
// rewritten to the opaque <hash>.env placeholder so the published artifact never leaks
|
||||
// the publisher's local path and stays consistent with the file-present case. The hash
|
||||
// derives from the path string alone, so it is deterministic regardless of existence.
|
||||
assert.Equal(t, len(envFiles), 0, "missing optional env file is not registered for upload")
|
||||
envPath := project.Services["web"].EnvFiles[0].Path
|
||||
hash := fmt.Sprintf("%x.env", sha256.Sum256([]byte(envPath)))
|
||||
assert.Assert(t, strings.Contains(string(data), hash), "published YAML should reference the hash placeholder")
|
||||
assert.Assert(t, !strings.Contains(string(data), "path: missing.env"), "published YAML must not leak the local path")
|
||||
assert.Assert(t, strings.Contains(string(data), "required: false"), "optional flag must be preserved")
|
||||
}
|
||||
|
||||
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_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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue