From ef314b3686ead9af898666d35765017577d02d7e Mon Sep 17 00:00:00 2001 From: Ishwar Date: Thu, 2 Apr 2026 18:32:41 +0530 Subject: [PATCH 1/4] fix(publish): detect named volumes with driver_opts bind in checkForBindMount Signed-off-by: Ishwar --- h --force-with-lease | 11 ++++ pkg/compose/publish.go | 36 +++++++++++- pkg/compose/publish_test.go | 113 ++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 h --force-with-lease diff --git a/h --force-with-lease b/h --force-with-lease new file mode 100644 index 0000000000..b7489df0e4 --- /dev/null +++ b/h --force-with-lease @@ -0,0 +1,11 @@ +df5d68b7e919c7b1ab5ff4fcf6514cec7c1c590c fix: update e2e tests to expect exit code 130 on user decline +Signed-off-by: Ishwar + +cbf7940a73f0e0ee8a52d3d30a13054eee46aae4 test: repurpose decline test to cover sensitive data detection path +Renames test to Test_preChecks_sensitive_data_detected_decline. Uses a temporary .env file with an AWS token to reliably trigger the DefangLabs secret detector, and confirms that preChecks correctly aborts early on user decline. + +Signed-off-by: Ishwar + +61e910218b2b2f0d519058a81d56a307ea2afc73 publish: return ErrPublishAborted when user declines interactive prompts +Signed-off-by: Ishwar + diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index 30b6a6a49b..f4118925bb 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -407,10 +407,23 @@ func (s *composeService) checkOnlyBuildSection(project *types.Project) (bool, er func (s *composeService) checkForBindMount(project *types.Project) map[string][]types.ServiceVolumeConfig { allFindings := map[string][]types.ServiceVolumeConfig{} for serviceName, config := range project.Services { - bindMounts := []types.ServiceVolumeConfig{} + var bindMounts []types.ServiceVolumeConfig for _, volume := range config.Volumes { if volume.Type == types.VolumeTypeBind { bindMounts = append(bindMounts, volume) + continue + } + if volume.Type == types.VolumeTypeVolume && volume.Source != "" { + if topLevel, ok := project.Volumes[volume.Source]; ok { + if isDriverOptsBind(topLevel) { + device := topLevel.DriverOpts["device"] + bindMounts = append(bindMounts, types.ServiceVolumeConfig{ + Type: types.VolumeTypeBind, + Source: device, + Target: volume.Target, + }) + } + } } } if len(bindMounts) > 0 { @@ -420,6 +433,27 @@ func (s *composeService) checkForBindMount(project *types.Project) map[string][] return allFindings } +func isDriverOptsBind(v types.VolumeConfig) bool { + if v.Driver != "" && v.Driver != "local" { + return false + } + opts := v.DriverOpts + if len(opts) == 0 { + return false + } + _, hasDevice := opts["device"] + if !hasDevice { + return false + } + for _, opt := range strings.Split(opts["o"], ",") { + switch strings.TrimSpace(opt) { + case "bind", "rbind": + return true + } + } + return false +} + func (s *composeService) checkForSensitiveData(ctx context.Context, project *types.Project) ([]secrets.DetectedSecret, error) { var allFindings []secrets.DetectedSecret scan := scanner.NewDefaultScanner() diff --git a/pkg/compose/publish_test.go b/pkg/compose/publish_test.go index 3b0e5a4389..4b6746f7da 100644 --- a/pkg/compose/publish_test.go +++ b/pkg/compose/publish_test.go @@ -163,3 +163,116 @@ func Test_publish_decline_returns_ErrCanceled(t *testing.T) { assert.Assert(t, errors.Is(err, api.ErrCanceled), "expected api.ErrCanceled when user declines, got: %v", err) } + +func Test_checkForBindMount_namedVolume_driverOptsBind(t *testing.T) { + project := &types.Project{ + Services: types.Services{ + "web": { + Name: "web", + Image: "nginx", + Volumes: []types.ServiceVolumeConfig{ + { + Type: types.VolumeTypeVolume, + Source: "secret_host_data", + Target: "/mnt/data", + }, + { + Type: types.VolumeTypeVolume, + Source: "normal_vol", + Target: "/data", + }, + }, + }, + }, + Volumes: types.Volumes{ + "secret_host_data": { + Driver: "local", + DriverOpts: map[string]string{ + "type": "none", + "o": "bind", + "device": "/Users/admin/.ssh", + }, + }, + "normal_vol": { + Driver: "local", + }, + }, + } + + svc := &composeService{} + findings := svc.checkForBindMount(project) + + assert.Equal(t, 1, len(findings["web"]), "expected exactly one bind mount finding for web") + assert.Equal(t, "/Users/admin/.ssh", findings["web"][0].Source) + assert.Equal(t, "/mnt/data", findings["web"][0].Target) +} + +func Test_isDriverOptsBind(t *testing.T) { + tests := []struct { + name string + volume types.VolumeConfig + expected bool + }{ + { + name: "plain bind", + volume: types.VolumeConfig{ + Driver: "local", + DriverOpts: map[string]string{"o": "bind", "device": "/host/path"}, + }, + expected: true, + }, + { + name: "rbind", + volume: types.VolumeConfig{ + Driver: "local", + DriverOpts: map[string]string{"o": "rbind", "device": "/host/path"}, + }, + expected: true, + }, + { + name: "comma-separated ro,bind — validates split logic", + volume: types.VolumeConfig{ + Driver: "local", + DriverOpts: map[string]string{"o": "ro,bind", "device": "/host/path"}, + }, + expected: true, + }, + { + name: "nobind must not match — guards against substring false positive", + volume: types.VolumeConfig{ + Driver: "local", + DriverOpts: map[string]string{"o": "nobind", "device": "/host/path"}, + }, + expected: false, + }, + { + name: "no device key", + volume: types.VolumeConfig{ + Driver: "local", + DriverOpts: map[string]string{"o": "bind"}, + }, + expected: false, + }, + { + name: "empty driver_opts", + volume: types.VolumeConfig{ + Driver: "local", + }, + expected: false, + }, + { + name: "non-local driver", + volume: types.VolumeConfig{ + Driver: "nfs", + DriverOpts: map[string]string{"o": "bind", "device": "/host/path"}, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, isDriverOptsBind(tt.volume)) + }) + } +} From 46b071ecb271739e00eee5c0886eb9a3e14234b2 Mon Sep 17 00:00:00 2001 From: Ishwar Date: Thu, 2 Apr 2026 18:46:38 +0530 Subject: [PATCH 2/4] chore: remove accidentally committed scratch file Signed-off-by: Ishwar --- h --force-with-lease | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 h --force-with-lease diff --git a/h --force-with-lease b/h --force-with-lease deleted file mode 100644 index b7489df0e4..0000000000 --- a/h --force-with-lease +++ /dev/null @@ -1,11 +0,0 @@ -df5d68b7e919c7b1ab5ff4fcf6514cec7c1c590c fix: update e2e tests to expect exit code 130 on user decline -Signed-off-by: Ishwar - -cbf7940a73f0e0ee8a52d3d30a13054eee46aae4 test: repurpose decline test to cover sensitive data detection path -Renames test to Test_preChecks_sensitive_data_detected_decline. Uses a temporary .env file with an AWS token to reliably trigger the DefangLabs secret detector, and confirms that preChecks correctly aborts early on user decline. - -Signed-off-by: Ishwar - -61e910218b2b2f0d519058a81d56a307ea2afc73 publish: return ErrPublishAborted when user declines interactive prompts -Signed-off-by: Ishwar - From ed6ab77115cfd277b7d15820b01fef592c361684 Mon Sep 17 00:00:00 2001 From: Ishwar Date: Thu, 2 Apr 2026 18:50:07 +0530 Subject: [PATCH 3/4] fix: address review comments - preserve volume flags and validate device Signed-off-by: Ishwar --- pkg/compose/publish.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index f4118925bb..c6c73839f6 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -416,11 +416,14 @@ func (s *composeService) checkForBindMount(project *types.Project) map[string][] if volume.Type == types.VolumeTypeVolume && volume.Source != "" { if topLevel, ok := project.Volumes[volume.Source]; ok { if isDriverOptsBind(topLevel) { - device := topLevel.DriverOpts["device"] + device := strings.TrimSpace(topLevel.DriverOpts["device"]) bindMounts = append(bindMounts, types.ServiceVolumeConfig{ - Type: types.VolumeTypeBind, - Source: device, - Target: volume.Target, + Type: types.VolumeTypeBind, + Source: device, + Target: volume.Target, + ReadOnly: volume.ReadOnly, + Consistency: volume.Consistency, + Bind: volume.Bind, }) } } @@ -441,8 +444,8 @@ func isDriverOptsBind(v types.VolumeConfig) bool { if len(opts) == 0 { return false } - _, hasDevice := opts["device"] - if !hasDevice { + device := strings.TrimSpace(opts["device"]) + if device == "" { return false } for _, opt := range strings.Split(opts["o"], ",") { From f8b928c7a5616e2ac68acdf667073d8584a2c96d Mon Sep 17 00:00:00 2001 From: Ishwar Date: Sun, 5 Apr 2026 15:06:27 +0530 Subject: [PATCH 4/4] test: add empty device value case to Test_isDriverOptsBind Signed-off-by: Ishwar --- pkg/compose/publish_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/compose/publish_test.go b/pkg/compose/publish_test.go index 4b6746f7da..83e869b82e 100644 --- a/pkg/compose/publish_test.go +++ b/pkg/compose/publish_test.go @@ -253,6 +253,14 @@ func Test_isDriverOptsBind(t *testing.T) { }, expected: false, }, + { + name: "empty device value", + volume: types.VolumeConfig{ + Driver: "local", + DriverOpts: map[string]string{"o": "bind", "device": " "}, + }, + expected: false, + }, { name: "empty driver_opts", volume: types.VolumeConfig{