From b92ba00cfaa70b64250872aca7c2d857dc06e592 Mon Sep 17 00:00:00 2001 From: Abhisek Datta Date: Thu, 8 Jan 2026 14:11:10 +0530 Subject: [PATCH] refactor: Sandbox for separation of boundaries --- config/config_template_test.go | 2 +- go.mod | 9 ++- go.sum | 11 +++- guard/guard.go | 16 +++-- internal/flows/proxy_flow.go | 20 +++--- sandbox/{ => executor}/apply.go | 35 ++++++----- sandbox/platform/platform_darwin.go | 12 ++++ sandbox/platform/platform_linux.go | 16 +++++ sandbox/platform/platform_unsupported.go | 15 +++++ sandbox/platform/platform_windows.go | 16 +++++ .../{seatbelt => platform}/seatbelt_darwin.go | 38 +++++++----- .../translator_darwin.go | 51 ++++------------ sandbox/sandbox.go | 56 ++++++++++++++--- sandbox/sandbox_darwin.go | 61 ------------------- sandbox/sandbox_linux.go | 11 ---- sandbox/sandbox_unsupported.go | 11 ---- sandbox/sandbox_windows.go | 11 ---- 17 files changed, 197 insertions(+), 194 deletions(-) rename sandbox/{ => executor}/apply.go (63%) create mode 100644 sandbox/platform/platform_darwin.go create mode 100644 sandbox/platform/platform_linux.go create mode 100644 sandbox/platform/platform_unsupported.go create mode 100644 sandbox/platform/platform_windows.go rename sandbox/{seatbelt => platform}/seatbelt_darwin.go (56%) rename sandbox/{seatbelt => platform}/translator_darwin.go (82%) delete mode 100644 sandbox/sandbox_darwin.go delete mode 100644 sandbox/sandbox_linux.go delete mode 100644 sandbox/sandbox_unsupported.go delete mode 100644 sandbox/sandbox_windows.go diff --git a/config/config_template_test.go b/config/config_template_test.go index 99aed46..5e7541c 100644 --- a/config/config_template_test.go +++ b/config/config_template_test.go @@ -6,7 +6,7 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/assert" - "go.yaml.in/yaml/v3" + "gopkg.in/yaml.v3" ) func TestTemplateParsesAsYAML(t *testing.T) { diff --git a/go.mod b/go.mod index a0a3413..2174db5 100644 --- a/go.mod +++ b/go.mod @@ -13,15 +13,16 @@ require ( github.com/google/osv-scalibr v0.2.1 github.com/google/uuid v1.6.0 github.com/jedib0t/go-pretty/v6 v6.6.7 - github.com/mitchellh/mapstructure v1.5.0 github.com/posthog/posthog-go v1.5.12 github.com/safedep/dry v0.0.0-20250514080944-bb77f30c7175 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 + go.yaml.in/yaml/v3 v3.0.4 + golang.org/x/sys v0.34.0 google.golang.org/grpc v1.72.0 - google.golang.org/protobuf v1.36.6 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -207,23 +208,21 @@ require ( go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect golang.org/x/mod v0.26.0 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect golang.org/x/term v0.33.0 // indirect golang.org/x/text v0.28.0 // indirect golang.org/x/tools v0.35.0 // indirect golang.org/x/tools/go/expect v0.1.1-deprecated // indirect golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.6.1 // indirect mvdan.cc/gofumpt v0.7.0 // indirect mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect diff --git a/go.sum b/go.sum index 2969483..91b941a 100644 --- a/go.sum +++ b/go.sum @@ -84,6 +84,8 @@ github.com/ckaznocha/intrange v0.3.0 h1:VqnxtK32pxgkhJgYQEeOArVidIPg+ahLP7WBOXZd github.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= @@ -102,7 +104,12 @@ github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVo github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -308,8 +315,6 @@ github.com/mgechev/revive v1.7.0 h1:JyeQ4yO5K8aZhIKf5rec56u0376h8AlKNQEmjfkjKlY= github.com/mgechev/revive v1.7.0/go.mod h1:qZnwcNhoguE58dfi96IJeSTPeZQejNeoMQLUZGi4SW4= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -343,6 +348,8 @@ github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/guard/guard.go b/guard/guard.go index 4608d2d..6765583 100644 --- a/guard/guard.go +++ b/guard/guard.go @@ -17,7 +17,7 @@ import ( "github.com/safedep/pmg/internal/eventlog" "github.com/safedep/pmg/internal/ui" "github.com/safedep/pmg/packagemanager" - "github.com/safedep/pmg/sandbox" + "github.com/safedep/pmg/sandbox/executor" ) type PackageManagerGuardInteraction struct { @@ -223,13 +223,19 @@ func (g *packageManagerGuard) continueExecution(ctx context.Context, pc *package // Apply sandbox if enabled pmName := g.packageManager.Name() - if err := sandbox.ApplySandbox(ctx, cmd, pmName, ""); err != nil { + result, err := executor.ApplySandbox(ctx, cmd, pmName, "") + if err != nil { return fmt.Errorf("failed to apply sandbox: %w", err) } - // We will fail based on executed command's exit code. This is important - // because other tools (scripts, CI etc.) may depend on this exit code. - return cmd.Run() + // Only run the command if the sandbox didn't already execute it + if result.ShouldRun() { + // We will fail based on executed command's exit code. This is important + // because other tools (scripts, CI etc.) may depend on this exit code. + return cmd.Run() + } + + return nil } func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context, diff --git a/internal/flows/proxy_flow.go b/internal/flows/proxy_flow.go index d6b0f1e..5903a8e 100644 --- a/internal/flows/proxy_flow.go +++ b/internal/flows/proxy_flow.go @@ -17,7 +17,7 @@ import ( "github.com/safedep/pmg/proxy" "github.com/safedep/pmg/proxy/certmanager" "github.com/safedep/pmg/proxy/interceptors" - "github.com/safedep/pmg/sandbox" + "github.com/safedep/pmg/sandbox/executor" ) type proxyFlow struct { @@ -244,7 +244,8 @@ func (f *proxyFlow) executeWithProxy(ctx context.Context, parsedCmd *packagemana // Apply sandbox if enabled (sandbox preserves proxy environment variables already set on cmd.Env) pmName := f.pm.Name() - if err := sandbox.ApplySandbox(ctx, cmd, pmName, "proxy mode"); err != nil { + result, err := executor.ApplySandbox(ctx, cmd, pmName, "proxy mode") + if err != nil { return fmt.Errorf("failed to apply sandbox: %w", err) } @@ -271,13 +272,16 @@ func (f *proxyFlow) executeWithProxy(ctx context.Context, parsedCmd *packagemana }, }) - err := cmd.Run() - if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - os.Exit(exitErr.ExitCode()) - } + // Only run the command if the sandbox didn't already execute it + if result.ShouldRun() { + err = cmd.Run() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + os.Exit(exitErr.ExitCode()) + } - return fmt.Errorf("failed to execute %s: %w", f.pm.Name(), err) + return fmt.Errorf("failed to execute %s: %w", f.pm.Name(), err) + } } log.Debugf("Command completed successfully") diff --git a/sandbox/apply.go b/sandbox/executor/apply.go similarity index 63% rename from sandbox/apply.go rename to sandbox/executor/apply.go index 1f76974..ed8f813 100644 --- a/sandbox/apply.go +++ b/sandbox/executor/apply.go @@ -1,4 +1,4 @@ -package sandbox +package executor import ( "context" @@ -7,6 +7,8 @@ import ( "github.com/safedep/dry/log" "github.com/safedep/pmg/config" + "github.com/safedep/pmg/sandbox" + "github.com/safedep/pmg/sandbox/platform" ) // ApplySandbox applies sandbox isolation to the command if sandbox mode is enabled. @@ -18,44 +20,48 @@ import ( // - pmName: Package manager name (e.g., "npm", "pip") used to determine the sandbox policy to apply // - mode: Optional mode description for logging (e.g., "proxy mode", empty for default) // -// Returns an error if sandbox setup fails, or nil if sandbox is not enabled/available. +// Returns: +// - ExecutionResult: Contains execution state. Callers must check result.ShouldRun() before calling cmd.Run(). +// - error: Non-nil if sandbox setup fails +// +// If sandbox is not enabled/available, returns a result indicating the caller should run the command. // Gracefully degrades with warnings if sandbox is unavailable on the platform. -func ApplySandbox(ctx context.Context, cmd *exec.Cmd, pmName string, mode string) error { +func ApplySandbox(ctx context.Context, cmd *exec.Cmd, pmName string, mode string) (*sandbox.ExecutionResult, error) { cfg := config.Get() if !cfg.Config.Sandbox.Enabled { - return nil + return sandbox.NewExecutionResult(false), nil } // Lookup the sandbox policy for the package manager based on config policyRef, exists := cfg.Config.Sandbox.Policies[pmName] if !exists || !policyRef.Enabled { log.Debugf("No sandbox policy enabled for %s", pmName) - return nil + return sandbox.NewExecutionResult(false), nil } - registry := NewProfileRegistry() + registry := sandbox.NewProfileRegistry() policy, err := registry.GetProfile(policyRef.Profile) if err != nil { - return fmt.Errorf("failed to load sandbox policy %s: %w", policyRef.Profile, err) + return nil, fmt.Errorf("failed to load sandbox policy %s: %w", policyRef.Profile, err) } if !policy.AppliesToPackageManager(pmName) { log.Warnf("Sandbox policy %s does not apply to %s", policy.Name, pmName) - return nil + return sandbox.NewExecutionResult(false), nil } // Create platform-specific sandbox - sb, err := NewSandbox() + sb, err := platform.NewSandbox() if err != nil { log.Warnf("Sandbox not available on this platform: %v", err) log.Warnf("Continuing without sandbox protection") - return nil + return sandbox.NewExecutionResult(false), nil } if !sb.IsAvailable() { log.Warnf("Sandbox %s not available, running without sandbox", sb.Name()) - return nil + return sandbox.NewExecutionResult(false), nil } logMsg := fmt.Sprintf("Running %s in %s sandbox with policy %s", pmName, sb.Name(), policy.Name) @@ -65,9 +71,10 @@ func ApplySandbox(ctx context.Context, cmd *exec.Cmd, pmName string, mode string log.Infof("%s", logMsg) - if err := sb.Execute(ctx, cmd, policy); err != nil { - return fmt.Errorf("failed to setup sandbox: %w", err) + result, err := sb.Execute(ctx, cmd, policy) + if err != nil { + return nil, fmt.Errorf("failed to setup sandbox: %w", err) } - return nil + return result, nil } diff --git a/sandbox/platform/platform_darwin.go b/sandbox/platform/platform_darwin.go new file mode 100644 index 0000000..bbfde9b --- /dev/null +++ b/sandbox/platform/platform_darwin.go @@ -0,0 +1,12 @@ +//go:build darwin +// +build darwin + +package platform + +import "github.com/safedep/pmg/sandbox" + +// NewSandbox creates a platform-specific sandbox instance for macOS. +// Uses Seatbelt (sandbox-exec) for process isolation. +func NewSandbox() (sandbox.Sandbox, error) { + return newSeatbeltSandbox() +} diff --git a/sandbox/platform/platform_linux.go b/sandbox/platform/platform_linux.go new file mode 100644 index 0000000..aee0ff3 --- /dev/null +++ b/sandbox/platform/platform_linux.go @@ -0,0 +1,16 @@ +//go:build linux +// +build linux + +package platform + +import ( + "errors" + + "github.com/safedep/pmg/sandbox" +) + +// NewSandbox creates a platform-specific sandbox instance for Linux. +// TODO: Implement Bubblewrap or seccomp-bpf based sandbox. +func NewSandbox() (sandbox.Sandbox, error) { + return nil, errors.New("sandbox not yet implemented for Linux") +} diff --git a/sandbox/platform/platform_unsupported.go b/sandbox/platform/platform_unsupported.go new file mode 100644 index 0000000..5d839cb --- /dev/null +++ b/sandbox/platform/platform_unsupported.go @@ -0,0 +1,15 @@ +//go:build !darwin && !linux && !windows +// +build !darwin,!linux,!windows + +package platform + +import ( + "errors" + + "github.com/safedep/pmg/sandbox" +) + +// NewSandbox returns an error on unsupported platforms. +func NewSandbox() (sandbox.Sandbox, error) { + return nil, errors.New("sandbox not supported on this platform") +} diff --git a/sandbox/platform/platform_windows.go b/sandbox/platform/platform_windows.go new file mode 100644 index 0000000..88998ac --- /dev/null +++ b/sandbox/platform/platform_windows.go @@ -0,0 +1,16 @@ +//go:build windows +// +build windows + +package platform + +import ( + "errors" + + "github.com/safedep/pmg/sandbox" +) + +// NewSandbox creates a platform-specific sandbox instance for Windows. +// TODO: Implement AppContainer or Job Objects based sandbox. +func NewSandbox() (sandbox.Sandbox, error) { + return nil, errors.New("sandbox not yet implemented for Windows") +} diff --git a/sandbox/seatbelt/seatbelt_darwin.go b/sandbox/platform/seatbelt_darwin.go similarity index 56% rename from sandbox/seatbelt/seatbelt_darwin.go rename to sandbox/platform/seatbelt_darwin.go index d2dc90b..b8934ff 100644 --- a/sandbox/seatbelt/seatbelt_darwin.go +++ b/sandbox/platform/seatbelt_darwin.go @@ -1,7 +1,7 @@ //go:build darwin // +build darwin -package seatbelt +package platform import ( "context" @@ -10,40 +10,45 @@ import ( "os/exec" "github.com/safedep/dry/log" + "github.com/safedep/pmg/sandbox" ) -// SeatbeltSandbox implements the Sandbox interface using macOS Seatbelt (sandbox-exec). -type SeatbeltSandbox struct { - translator *PolicyTranslator +// seatbeltSandbox implements the Sandbox interface using macOS Seatbelt (sandbox-exec). +type seatbeltSandbox struct { + translator *policyTranslator } -// NewSeatbeltSandbox creates a new Seatbelt sandbox instance. -func NewSeatbeltSandbox() (*SeatbeltSandbox, error) { - return &SeatbeltSandbox{ - translator: NewPolicyTranslator(), +// newSeatbeltSandbox creates a new Seatbelt sandbox instance. +func newSeatbeltSandbox() (*seatbeltSandbox, error) { + return &seatbeltSandbox{ + translator: newPolicyTranslator(), }, nil } // Execute runs a command in the Seatbelt sandbox with the given policy. // It translates the PMG policy to Seatbelt Profile Language (.sb) and wraps // the command execution with sandbox-exec. -func (s *SeatbeltSandbox) Execute(ctx context.Context, cmd *exec.Cmd, policy *SandboxPolicy) error { +// +// This implementation modifies the cmd in place and does NOT execute it. +// Returns ExecutionResult with executed=false, indicating the caller must run cmd.Run(). +func (s *seatbeltSandbox) Execute(ctx context.Context, cmd *exec.Cmd, policy *sandbox.SandboxPolicy) (*sandbox.ExecutionResult, error) { // Translate PMG policy to Seatbelt profile - sbProfile, err := s.translator.Translate(policy) + sbProfile, err := s.translator.translate(policy) if err != nil { - return fmt.Errorf("failed to translate sandbox policy: %w", err) + return nil, fmt.Errorf("failed to translate sandbox policy: %w", err) } // Write Seatbelt profile to temporary file tmpFile, err := os.CreateTemp("", "pmg-sandbox-*.sb") if err != nil { - return fmt.Errorf("failed to create temporary sandbox profile: %w", err) + return nil, fmt.Errorf("failed to create temporary sandbox profile: %w", err) } + defer os.Remove(tmpFile.Name()) if _, err := tmpFile.WriteString(sbProfile); err != nil { tmpFile.Close() - return fmt.Errorf("failed to write sandbox profile: %w", err) + return nil, fmt.Errorf("failed to write sandbox profile: %w", err) } tmpFile.Close() @@ -69,16 +74,17 @@ func (s *SeatbeltSandbox) Execute(ctx context.Context, cmd *exec.Cmd, policy *Sa log.Debugf("Sandboxed command: %s %v", cmd.Path, cmd.Args) - return nil + // Return ExecutionResult indicating we only modified cmd, didn't execute it + return sandbox.NewExecutionResult(false), nil } // Name returns the name of this sandbox implementation. -func (s *SeatbeltSandbox) Name() string { +func (s *seatbeltSandbox) Name() string { return "seatbelt" } // IsAvailable returns true if sandbox-exec is available on this system. -func (s *SeatbeltSandbox) IsAvailable() bool { +func (s *seatbeltSandbox) IsAvailable() bool { _, err := exec.LookPath("sandbox-exec") return err == nil } diff --git a/sandbox/seatbelt/translator_darwin.go b/sandbox/platform/translator_darwin.go similarity index 82% rename from sandbox/seatbelt/translator_darwin.go rename to sandbox/platform/translator_darwin.go index 02ab180..dcd8c7d 100644 --- a/sandbox/seatbelt/translator_darwin.go +++ b/sandbox/platform/translator_darwin.go @@ -1,54 +1,27 @@ //go:build darwin // +build darwin -package seatbelt +package platform import ( "fmt" "path/filepath" "strings" + "github.com/safedep/pmg/sandbox" "github.com/safedep/pmg/sandbox/util" ) -// PolicyTranslator translates PMG sandbox policies to Seatbelt Profile Language (.sb). -type PolicyTranslator struct{} +// policyTranslator translates PMG sandbox policies to Seatbelt Profile Language (.sb). +type policyTranslator struct{} -// NewPolicyTranslator creates a new policy translator. -func NewPolicyTranslator() *PolicyTranslator { - return &PolicyTranslator{} +// newPolicyTranslator creates a new policy translator. +func newPolicyTranslator() *policyTranslator { + return &policyTranslator{} } -// SandboxPolicy represents a parsed sandbox policy (defined here to avoid import cycle). -type SandboxPolicy struct { - Name string - Description string - PackageManagers []string - ViolationMode string - Filesystem FilesystemPolicy - Network NetworkPolicy - Process ProcessPolicy -} - -type FilesystemPolicy struct { - AllowRead []string - AllowWrite []string - DenyRead []string - DenyWrite []string -} - -type NetworkPolicy struct { - AllowOutbound []string - DenyOutbound []string -} - -type ProcessPolicy struct { - AllowExec []string - DenyExec []string -} - -// Translate converts a PMG SandboxPolicy to Seatbelt Profile Language. -func (t *PolicyTranslator) Translate(policy *SandboxPolicy) (string, error) { +// translate converts a PMG SandboxPolicy to Seatbelt Profile Language. +func (t *policyTranslator) translate(policy *sandbox.SandboxPolicy) (string, error) { var sb strings.Builder // Header @@ -89,7 +62,7 @@ func (t *PolicyTranslator) Translate(policy *SandboxPolicy) (string, error) { } // translateFilesystem translates filesystem access rules. -func (t *PolicyTranslator) translateFilesystem(policy *SandboxPolicy, sb *strings.Builder) error { +func (t *policyTranslator) translateFilesystem(policy *sandbox.SandboxPolicy, sb *strings.Builder) error { sb.WriteString(";; Filesystem access\n") // Expand and add allow read rules @@ -164,7 +137,7 @@ func (t *PolicyTranslator) translateFilesystem(policy *SandboxPolicy, sb *string } // translateNetwork translates network access rules. -func (t *PolicyTranslator) translateNetwork(policy *SandboxPolicy, sb *strings.Builder) error { +func (t *policyTranslator) translateNetwork(policy *sandbox.SandboxPolicy, sb *strings.Builder) error { sb.WriteString(";; Network access\n") // If there are allow outbound rules, allow network-outbound generally @@ -192,7 +165,7 @@ func (t *PolicyTranslator) translateNetwork(policy *SandboxPolicy, sb *strings.B } // translateProcess translates process execution rules. -func (t *PolicyTranslator) translateProcess(policy *SandboxPolicy, sb *strings.Builder) error { +func (t *policyTranslator) translateProcess(policy *sandbox.SandboxPolicy, sb *strings.Builder) error { sb.WriteString(";; Process execution\n") // Add allow exec rules diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go index a5b75da..7505ac8 100644 --- a/sandbox/sandbox.go +++ b/sandbox/sandbox.go @@ -5,14 +5,56 @@ import ( "os/exec" ) +// ExecutionResult represents the result of applying a sandbox to a command. +// It encapsulates the execution state and allows for future extension with +// additional metadata (e.g., exit codes, resource usage, violation events). +type ExecutionResult struct { + executed bool + // Future fields can be added here without breaking the API: + // - exitCode int + // - resourceUsage ResourceStats + // - violations []ViolationEvent +} + +// NewExecutionResult creates a new ExecutionResult. +// If executed is true, it indicates the sandbox executed the command directly. +// If executed is false, the sandbox only modified the command and the caller must execute it. +func NewExecutionResult(executed bool) *ExecutionResult { + return &ExecutionResult{ + executed: executed, + } +} + +// WasExecuted returns true if the sandbox executed the command directly. +// If false, the caller must execute the command using cmd.Run(). +func (r *ExecutionResult) WasExecuted() bool { + return r.executed +} + +// ShouldRun returns true if the caller should execute cmd.Run(). +// This is the inverse of WasExecuted() and may be more intuitive at call sites. +func (r *ExecutionResult) ShouldRun() bool { + return !r.executed +} + // Sandbox represents a platform-specific sandbox executor that isolates // package manager processes with controlled access to filesystem, network, // and process execution resources. type Sandbox interface { - // Execute runs a command in the sandbox with the given policy. - // The command may be modified in place (e.g., wrapped with sandbox-exec). - // Returns an error if the sandbox setup fails. - Execute(ctx context.Context, cmd *exec.Cmd, policy *SandboxPolicy) error + // Execute prepares or runs a command in the sandbox with the given policy. + // + // Behavior varies by implementation: + // - CLI-based sandboxes (Seatbelt, Bubblewrap): Modify cmd in place by wrapping it + // with sandbox CLI (e.g., sandbox-exec). Returns ExecutionResult with executed=false. + // - Library-based sandboxes: Execute the command directly within the sandbox. + // Returns ExecutionResult with executed=true. + // + // Returns: + // - ExecutionResult: Contains execution state and metadata + // - error: Non-nil if sandbox setup or execution failed + // + // Callers must check result.ShouldRun() and only call cmd.Run() if true. + Execute(ctx context.Context, cmd *exec.Cmd, policy *SandboxPolicy) (*ExecutionResult, error) // Name returns the sandbox implementation name (e.g., "seatbelt", "bubblewrap"). Name() string @@ -21,12 +63,6 @@ type Sandbox interface { IsAvailable() bool } -// NewSandbox creates a new platform-specific sandbox instance. -// The implementation is selected at compile time using build tags. -// Returns an error if the sandbox is not available on the current platform. -func NewSandbox() (Sandbox, error) { - return newPlatformSandbox() -} // ProfileRegistry manages built-in and custom sandbox policies. type ProfileRegistry interface { diff --git a/sandbox/sandbox_darwin.go b/sandbox/sandbox_darwin.go deleted file mode 100644 index ae3f78e..0000000 --- a/sandbox/sandbox_darwin.go +++ /dev/null @@ -1,61 +0,0 @@ -//go:build darwin -// +build darwin - -package sandbox - -import ( - "context" - "os/exec" - - "github.com/safedep/pmg/sandbox/seatbelt" -) - -// darwinSandboxAdapter adapts the seatbelt implementation to the Sandbox interface. -type darwinSandboxAdapter struct { - seatbelt *seatbelt.SeatbeltSandbox -} - -// newPlatformSandbox creates a platform-specific sandbox instance for macOS. -// Uses Seatbelt (sandbox-exec) for process isolation. -func newPlatformSandbox() (Sandbox, error) { - sb, err := seatbelt.NewSeatbeltSandbox() - if err != nil { - return nil, err - } - - return &darwinSandboxAdapter{seatbelt: sb}, nil -} - -func (d *darwinSandboxAdapter) Execute(ctx context.Context, cmd *exec.Cmd, policy *SandboxPolicy) error { - // Convert sandbox.SandboxPolicy to seatbelt.SandboxPolicy - seatbeltPolicy := &seatbelt.SandboxPolicy{ - Name: policy.Name, - Description: policy.Description, - PackageManagers: policy.PackageManagers, - ViolationMode: policy.ViolationMode, - Filesystem: seatbelt.FilesystemPolicy{ - AllowRead: policy.Filesystem.AllowRead, - AllowWrite: policy.Filesystem.AllowWrite, - DenyRead: policy.Filesystem.DenyRead, - DenyWrite: policy.Filesystem.DenyWrite, - }, - Network: seatbelt.NetworkPolicy{ - AllowOutbound: policy.Network.AllowOutbound, - DenyOutbound: policy.Network.DenyOutbound, - }, - Process: seatbelt.ProcessPolicy{ - AllowExec: policy.Process.AllowExec, - DenyExec: policy.Process.DenyExec, - }, - } - - return d.seatbelt.Execute(ctx, cmd, seatbeltPolicy) -} - -func (d *darwinSandboxAdapter) Name() string { - return d.seatbelt.Name() -} - -func (d *darwinSandboxAdapter) IsAvailable() bool { - return d.seatbelt.IsAvailable() -} diff --git a/sandbox/sandbox_linux.go b/sandbox/sandbox_linux.go deleted file mode 100644 index 5e0b491..0000000 --- a/sandbox/sandbox_linux.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build linux -// +build linux - -package sandbox - -import "errors" - -// newPlatformSandbox creates a platform-specific sandbox instance for Linux. -func newPlatformSandbox() (Sandbox, error) { - return nil, errors.New("sandbox not yet implemented for Linux (coming soon: Bubblewrap or seccomp-bpf)") -} diff --git a/sandbox/sandbox_unsupported.go b/sandbox/sandbox_unsupported.go deleted file mode 100644 index 2b266b8..0000000 --- a/sandbox/sandbox_unsupported.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !darwin && !linux && !windows -// +build !darwin,!linux,!windows - -package sandbox - -import "fmt" - -// newPlatformSandbox returns an error on unsupported platforms. -func newPlatformSandbox() (Sandbox, error) { - return nil, fmt.Errorf("sandbox is not supported on this platform") -} diff --git a/sandbox/sandbox_windows.go b/sandbox/sandbox_windows.go deleted file mode 100644 index 4ea3b78..0000000 --- a/sandbox/sandbox_windows.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build windows -// +build windows - -package sandbox - -import "errors" - -// newPlatformSandbox creates a platform-specific sandbox instance for Windows. -func newPlatformSandbox() (Sandbox, error) { - return nil, errors.New("sandbox not yet implemented for Windows") -}