refactor: Sandbox for separation of boundaries

This commit is contained in:
Abhisek Datta
2026-01-08 14:11:10 +05:30
parent aa2a6cc214
commit b92ba00cfa
17 changed files with 197 additions and 194 deletions
+1 -1
View File
@@ -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) {
+4 -5
View File
@@ -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
+9 -2
View File
@@ -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=
+11 -5
View File
@@ -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,
+12 -8
View File
@@ -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")
+21 -14
View File
@@ -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
}
+12
View File
@@ -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()
}
+16
View File
@@ -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")
}
+15
View File
@@ -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")
}
+16
View File
@@ -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")
}
@@ -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
}
@@ -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
+46 -10
View File
@@ -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 {
-61
View File
@@ -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()
}
-11
View File
@@ -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)")
}
-11
View File
@@ -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")
}
-11
View File
@@ -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")
}