mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
refactor: Sandbox for separation of boundaries
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user