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:
@@ -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