mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: Sandbox implementation with seatbelt
This commit is contained in:
@@ -21,4 +21,8 @@ func ApplyCobraFlags(cmd *cobra.Command) {
|
||||
globalConfig.Config.SkipEventLogging, "Skip event logging")
|
||||
cmd.PersistentFlags().BoolVar(&globalConfig.Config.ExperimentalProxyMode, "experimental-proxy-mode",
|
||||
globalConfig.Config.ExperimentalProxyMode, "Use experimental proxy-based interception (EXPERIMENTAL)")
|
||||
cmd.PersistentFlags().BoolVar(&globalConfig.Config.Sandbox.Enabled, "sandbox",
|
||||
globalConfig.Config.Sandbox.Enabled, "Enable sandbox mode to isolate package manager processes (EXPERIMENTAL)")
|
||||
cmd.PersistentFlags().StringVar(&globalConfig.Config.Sandbox.ViolationMode, "sandbox-violation-mode",
|
||||
globalConfig.Config.Sandbox.ViolationMode, "How to handle sandbox policy violations: block, warn, or allow")
|
||||
}
|
||||
|
||||
@@ -59,6 +59,33 @@ type Config struct {
|
||||
// ExperimentalProxyMode enables experimental proxy-based package interception.
|
||||
// When enabled, PMG starts a proxy server and intercepts package manager requests in real-time.
|
||||
ExperimentalProxyMode bool `mapstructure:"experimental_proxy_mode"`
|
||||
|
||||
// Sandbox enables sandboxing of package manager processes with controlled filesystem,
|
||||
// network, and process execution access. Provides defense-in-depth against supply chain attacks.
|
||||
Sandbox SandboxConfig `mapstructure:"sandbox"`
|
||||
}
|
||||
|
||||
// SandboxConfig configures the sandbox system for isolating package manager processes.
|
||||
type SandboxConfig struct {
|
||||
// Enabled enables sandbox mode (opt-in by default for backward compatibility).
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
|
||||
// ViolationMode defines how policy violations are handled (block, warn, or allow).
|
||||
ViolationMode string `mapstructure:"violation_mode"`
|
||||
|
||||
// Policies maps package manager names to their sandbox policy references.
|
||||
// Key is package manager name (e.g., "npm", "pip"), value is policy reference.
|
||||
Policies map[string]SandboxPolicyRef `mapstructure:"policies"`
|
||||
}
|
||||
|
||||
// SandboxPolicyRef references a sandbox policy for a specific package manager.
|
||||
type SandboxPolicyRef struct {
|
||||
// Enabled enables sandboxing for this specific package manager.
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
|
||||
// Profile is the name of a built-in profile (e.g., "npm-restrictive")
|
||||
// or an absolute path to a custom YAML policy file.
|
||||
Profile string `mapstructure:"profile"`
|
||||
}
|
||||
|
||||
// TrustedPackage is a package that is trusted by the user and will be ignored by the security guardrails.
|
||||
|
||||
@@ -46,3 +46,70 @@ experimental_proxy_mode: false
|
||||
trusted_packages:
|
||||
- purl: pkg:npm/@safedep/pmg
|
||||
reason: "PMG is a trusted package for PMG"
|
||||
|
||||
# Sandbox configuration (EXPERIMENTAL)
|
||||
# When enabled, package managers run in sandboxed environments with restricted
|
||||
# filesystem, network, and process execution access. This provides defense-in-depth
|
||||
# protection against malicious install scripts and supply chain attacks.
|
||||
#
|
||||
# Currently supported platforms:
|
||||
# - macOS (using Seatbelt sandbox-exec)
|
||||
# - Linux (coming soon: Bubblewrap or seccomp-bpf)
|
||||
# - Windows (coming soon)
|
||||
sandbox:
|
||||
# Enable sandbox mode (opt-in, default: false for backward compatibility)
|
||||
enabled: false
|
||||
|
||||
# How to handle policy violations: block | warn | allow
|
||||
# - block: Prevent execution on policy violation (recommended)
|
||||
# - warn: Log warning but allow execution
|
||||
# - allow: Allow all operations (disables sandbox)
|
||||
violation_mode: block
|
||||
|
||||
# Per-package-manager sandbox policies
|
||||
# Each package manager can have its own policy to account for unique security characteristics
|
||||
policies:
|
||||
# npm ecosystem
|
||||
npm:
|
||||
enabled: true
|
||||
profile: npm-restrictive # Built-in profile or path to custom YAML
|
||||
|
||||
pnpm:
|
||||
enabled: true
|
||||
profile: npm-restrictive
|
||||
|
||||
yarn:
|
||||
enabled: true
|
||||
profile: npm-restrictive
|
||||
|
||||
bun:
|
||||
enabled: true
|
||||
profile: npm-restrictive
|
||||
|
||||
# PyPI ecosystem
|
||||
pip:
|
||||
enabled: true
|
||||
profile: pypi-restrictive
|
||||
|
||||
pip3:
|
||||
enabled: true
|
||||
profile: pypi-restrictive
|
||||
|
||||
poetry:
|
||||
enabled: true
|
||||
profile: pypi-restrictive
|
||||
|
||||
uv:
|
||||
enabled: true
|
||||
profile: pypi-restrictive
|
||||
|
||||
# Usage:
|
||||
# 1. Enable sandbox globally: set sandbox.enabled to true
|
||||
# 2. Enable via CLI flag: pmg --sandbox npm install lodash
|
||||
# 3. Use custom profile: pmg --sandbox-profile=/path/to/policy.yml npm install
|
||||
#
|
||||
# Built-in profiles:
|
||||
# - npm-restrictive: Balanced security for npm/pnpm/yarn/bun
|
||||
# - pypi-restrictive: Balanced security for pip/poetry/uv
|
||||
#
|
||||
# See sandbox/profiles/ directory for profile definitions
|
||||
|
||||
@@ -17,6 +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"
|
||||
)
|
||||
|
||||
type PackageManagerGuardInteraction struct {
|
||||
@@ -220,6 +221,12 @@ func (g *packageManagerGuard) continueExecution(ctx context.Context, pc *package
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// Apply sandbox if enabled
|
||||
pmName := g.packageManager.Name()
|
||||
if err := sandbox.ApplySandbox(ctx, cmd, pmName, ""); 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()
|
||||
|
||||
@@ -17,6 +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"
|
||||
)
|
||||
|
||||
type proxyFlow struct {
|
||||
@@ -241,6 +242,12 @@ func (f *proxyFlow) executeWithProxy(ctx context.Context, parsedCmd *packagemana
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf("failed to apply sandbox: %w", err)
|
||||
}
|
||||
|
||||
log.Debugf("Executing command: %s %v", parsedCmd.Command.Exe, parsedCmd.Command.Args)
|
||||
log.Debugf("Proxy environment: HTTP_PROXY=%s, HTTPS_PROXY=%s, NODE_EXTRA_CA_CERTS=%s", proxyURL, proxyURL, caCertPath)
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/pmg/config"
|
||||
)
|
||||
|
||||
// ApplySandbox applies sandbox isolation to the command if sandbox mode is enabled.
|
||||
// This is a helper function used by both guard and proxy flows to avoid code duplication.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for the sandbox execution
|
||||
// - cmd: The exec.Cmd to be sandboxed (will be modified in place)
|
||||
// - pmName: Package manager name (e.g., "npm", "pip")
|
||||
// - 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.
|
||||
// Gracefully degrades with warnings if sandbox is unavailable on the platform.
|
||||
func ApplySandbox(ctx context.Context, cmd *exec.Cmd, pmName string, mode string) error {
|
||||
cfg := config.Get()
|
||||
|
||||
// Check if sandbox is enabled globally
|
||||
if !cfg.Config.Sandbox.Enabled {
|
||||
return nil // Sandbox disabled, skip
|
||||
}
|
||||
|
||||
// Check if sandbox policy exists for this package manager
|
||||
policyRef, exists := cfg.Config.Sandbox.Policies[pmName]
|
||||
if !exists || !policyRef.Enabled {
|
||||
log.Debugf("No sandbox policy enabled for %s", pmName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load the sandbox policy
|
||||
registry := NewProfileRegistry()
|
||||
policy, err := registry.GetProfile(policyRef.Profile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load sandbox policy %s: %w", policyRef.Profile, err)
|
||||
}
|
||||
|
||||
// Validate that the policy applies to this package manager
|
||||
if !policy.AppliesToPackageManager(pmName) {
|
||||
log.Warnf("Sandbox policy %s does not apply to %s", policy.Name, pmName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create platform-specific sandbox
|
||||
sb, err := NewSandbox()
|
||||
if err != nil {
|
||||
// Sandbox not available on this platform - log warning and continue
|
||||
log.Warnf("Sandbox not available on this platform: %v", err)
|
||||
log.Warnf("Continuing without sandbox protection")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !sb.IsAvailable() {
|
||||
log.Warnf("Sandbox %s not available, running without sandbox", sb.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build log message with optional mode suffix
|
||||
logMsg := fmt.Sprintf("Running %s in %s sandbox with policy %s", pmName, sb.Name(), policy.Name)
|
||||
if mode != "" {
|
||||
logMsg += fmt.Sprintf(" (%s)", mode)
|
||||
}
|
||||
log.Infof(logMsg)
|
||||
|
||||
// Execute sandbox setup (modifies cmd in place)
|
||||
// Note: The sandbox preserves any environment variables already set on cmd.Env
|
||||
if err := sb.Execute(ctx, cmd, policy); err != nil {
|
||||
return fmt.Errorf("failed to setup sandbox: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SandboxPolicy represents a parsed and validated sandbox policy that defines
|
||||
// filesystem, network, and process execution restrictions for package managers.
|
||||
type SandboxPolicy struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
PackageManagers []string `yaml:"package_managers" json:"package_managers"`
|
||||
ViolationMode string `yaml:"violation_mode" json:"violation_mode"`
|
||||
Filesystem FilesystemPolicy `yaml:"filesystem" json:"filesystem"`
|
||||
Network NetworkPolicy `yaml:"network" json:"network"`
|
||||
Process ProcessPolicy `yaml:"process" json:"process"`
|
||||
}
|
||||
|
||||
// FilesystemPolicy defines allowed and denied filesystem access patterns.
|
||||
// Deny rules have higher priority than allow rules.
|
||||
type FilesystemPolicy struct {
|
||||
AllowRead []string `yaml:"allow_read" json:"allow_read"`
|
||||
AllowWrite []string `yaml:"allow_write" json:"allow_write"`
|
||||
DenyRead []string `yaml:"deny_read" json:"deny_read"`
|
||||
DenyWrite []string `yaml:"deny_write" json:"deny_write"`
|
||||
}
|
||||
|
||||
// NetworkPolicy defines allowed and denied network access patterns.
|
||||
// Patterns are in the format "host:port" or "*:*" for wildcards.
|
||||
type NetworkPolicy struct {
|
||||
AllowOutbound []string `yaml:"allow_outbound" json:"allow_outbound"`
|
||||
DenyOutbound []string `yaml:"deny_outbound" json:"deny_outbound"`
|
||||
}
|
||||
|
||||
// ProcessPolicy defines allowed and denied process execution patterns.
|
||||
// Patterns can be specific paths or glob patterns.
|
||||
type ProcessPolicy struct {
|
||||
AllowExec []string `yaml:"allow_exec" json:"allow_exec"`
|
||||
DenyExec []string `yaml:"deny_exec" json:"deny_exec"`
|
||||
}
|
||||
|
||||
// ViolationMode represents how sandbox policy violations are handled.
|
||||
type ViolationMode int
|
||||
|
||||
const (
|
||||
// ViolationModeBlock blocks execution when a policy violation is detected (default).
|
||||
ViolationModeBlock ViolationMode = iota
|
||||
// ViolationModeWarn logs a warning but allows execution to continue.
|
||||
ViolationModeWarn
|
||||
// ViolationModeAllow allows all operations (effectively disables the sandbox).
|
||||
ViolationModeAllow
|
||||
)
|
||||
|
||||
// String returns the string representation of the violation mode.
|
||||
func (v ViolationMode) String() string {
|
||||
switch v {
|
||||
case ViolationModeBlock:
|
||||
return "block"
|
||||
case ViolationModeWarn:
|
||||
return "warn"
|
||||
case ViolationModeAllow:
|
||||
return "allow"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// ParseViolationMode parses a violation mode string into a ViolationMode value.
|
||||
func ParseViolationMode(s string) (ViolationMode, error) {
|
||||
switch strings.ToLower(s) {
|
||||
case "block":
|
||||
return ViolationModeBlock, nil
|
||||
case "warn":
|
||||
return ViolationModeWarn, nil
|
||||
case "allow":
|
||||
return ViolationModeAllow, nil
|
||||
default:
|
||||
return ViolationModeBlock, fmt.Errorf("invalid violation mode: %s (must be block, warn, or allow)", s)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates the sandbox policy for correctness.
|
||||
// Returns an error if the policy is invalid.
|
||||
func (p *SandboxPolicy) Validate() error {
|
||||
if p.Name == "" {
|
||||
return fmt.Errorf("policy name is required")
|
||||
}
|
||||
|
||||
if len(p.PackageManagers) == 0 {
|
||||
return fmt.Errorf("policy must specify at least one package manager")
|
||||
}
|
||||
|
||||
// Validate violation mode
|
||||
if p.ViolationMode != "" {
|
||||
if _, err := ParseViolationMode(p.ViolationMode); err != nil {
|
||||
return fmt.Errorf("invalid violation mode: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that at least some rules are defined
|
||||
hasRules := len(p.Filesystem.AllowRead) > 0 ||
|
||||
len(p.Filesystem.AllowWrite) > 0 ||
|
||||
len(p.Filesystem.DenyRead) > 0 ||
|
||||
len(p.Filesystem.DenyWrite) > 0 ||
|
||||
len(p.Network.AllowOutbound) > 0 ||
|
||||
len(p.Network.DenyOutbound) > 0 ||
|
||||
len(p.Process.AllowExec) > 0 ||
|
||||
len(p.Process.DenyExec) > 0
|
||||
|
||||
if !hasRules {
|
||||
return fmt.Errorf("policy must define at least one access rule")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetViolationMode returns the parsed violation mode for the policy.
|
||||
// Returns ViolationModeBlock if not specified or invalid.
|
||||
func (p *SandboxPolicy) GetViolationMode() ViolationMode {
|
||||
if p.ViolationMode == "" {
|
||||
return ViolationModeBlock
|
||||
}
|
||||
|
||||
mode, err := ParseViolationMode(p.ViolationMode)
|
||||
if err != nil {
|
||||
return ViolationModeBlock
|
||||
}
|
||||
|
||||
return mode
|
||||
}
|
||||
|
||||
// AppliesToPackageManager returns true if this policy applies to the given package manager.
|
||||
func (p *SandboxPolicy) AppliesToPackageManager(pm string) bool {
|
||||
pmLower := strings.ToLower(pm)
|
||||
for _, supported := range p.PackageManagers {
|
||||
if strings.ToLower(supported) == pmLower {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# PMG Sandbox Profiles
|
||||
|
||||
This directory contains built-in sandbox policies for PMG package managers.
|
||||
|
||||
## Available Profiles
|
||||
|
||||
### npm-restrictive
|
||||
|
||||
Restrictive policy for the npm ecosystem (npm, pnpm, yarn, bun).
|
||||
|
||||
**Features:**
|
||||
- Allows read access to current directory, package manager configs, and caches
|
||||
- Restricts write access to `node_modules/` and lockfiles only
|
||||
- Blocks access to sensitive files (`~/.ssh`, `~/.aws`, `.env` files)
|
||||
- Allows network access to npm registries only
|
||||
- Permits Node.js and git execution, blocks shell and curl/wget
|
||||
|
||||
**Use when:** You want balanced protection for npm package installations
|
||||
|
||||
### pypi-restrictive
|
||||
|
||||
Restrictive policy for the PyPI ecosystem (pip, pip3, poetry, uv).
|
||||
|
||||
**Features:**
|
||||
- Allows read access to current directory, pip configs, and caches
|
||||
- Restricts write access to virtual environments and package caches
|
||||
- Blocks access to sensitive files (`~/.ssh`, `~/.aws`, `.env` files)
|
||||
- Allows network access to PyPI registries only
|
||||
- Permits Python, compilers (for native extensions), and git
|
||||
|
||||
**Use when:** You want balanced protection for pip package installations
|
||||
|
||||
## Custom Policies
|
||||
|
||||
You can create custom sandbox policies by:
|
||||
|
||||
1. Copying one of the built-in profiles
|
||||
2. Modifying the rules to suit your needs
|
||||
3. Referencing the custom profile in your PMG config:
|
||||
|
||||
```yaml
|
||||
sandbox:
|
||||
enabled: true
|
||||
policies:
|
||||
npm:
|
||||
enabled: true
|
||||
profile: /path/to/custom-npm-policy.yml
|
||||
```
|
||||
|
||||
## Policy Schema
|
||||
|
||||
See the [Policy Schema Documentation](../policy.go) for details on the YAML structure.
|
||||
|
||||
### Supported Variables
|
||||
|
||||
- `${HOME}`: User home directory
|
||||
- `${CWD}`: Current working directory
|
||||
- `${PM_CACHE}`: Package manager cache directory
|
||||
- `${TMPDIR}`: Temporary directory
|
||||
|
||||
### Violation Modes
|
||||
|
||||
- `block`: Block execution on policy violation (recommended)
|
||||
- `warn`: Log warning but allow execution
|
||||
- `allow`: Allow all operations (disables sandbox)
|
||||
@@ -0,0 +1,73 @@
|
||||
name: npm-restrictive
|
||||
description: Restrictive sandbox policy for npm ecosystem (npm, pnpm, yarn, bun)
|
||||
package_managers:
|
||||
- npm
|
||||
- pnpm
|
||||
- yarn
|
||||
- bun
|
||||
|
||||
violation_mode: block
|
||||
|
||||
filesystem:
|
||||
allow_read:
|
||||
- ${CWD}/**
|
||||
- ${HOME}/.npmrc
|
||||
- ${HOME}/.yarnrc
|
||||
- ${HOME}/.yarnrc.yml
|
||||
- ${HOME}/.bundle
|
||||
- ${PM_CACHE}/**
|
||||
- /usr/local/**
|
||||
- /Library/**
|
||||
- /System/Library/**
|
||||
- /private/var/**
|
||||
|
||||
allow_write:
|
||||
- ${CWD}/node_modules/**
|
||||
- ${PM_CACHE}/**
|
||||
- ${CWD}/package-lock.json
|
||||
- ${CWD}/yarn.lock
|
||||
- ${CWD}/pnpm-lock.yaml
|
||||
- ${CWD}/bun.lockb
|
||||
- ${TMPDIR}/**
|
||||
|
||||
deny_read:
|
||||
- ${HOME}/.ssh/**
|
||||
- ${HOME}/.aws/**
|
||||
- ${HOME}/.gcloud/**
|
||||
- ${HOME}/.kube/**
|
||||
- "**/.env"
|
||||
- "**/.env.*"
|
||||
- ${HOME}/.docker/config.json
|
||||
|
||||
deny_write:
|
||||
- ${HOME}/.ssh/**
|
||||
- ${HOME}/.aws/**
|
||||
- /etc/**
|
||||
- /usr/**
|
||||
- /bin/**
|
||||
- /sbin/**
|
||||
|
||||
network:
|
||||
allow_outbound:
|
||||
- registry.npmjs.org:443
|
||||
- registry.yarnpkg.com:443
|
||||
- npm.pkg.github.com:443
|
||||
- github.com:443
|
||||
|
||||
deny_outbound:
|
||||
- "*:*"
|
||||
|
||||
process:
|
||||
allow_exec:
|
||||
- /usr/bin/node
|
||||
- /usr/local/bin/node
|
||||
- ${PM_CACHE}/**
|
||||
- /usr/bin/git
|
||||
- /usr/local/bin/git
|
||||
|
||||
deny_exec:
|
||||
- /usr/bin/curl
|
||||
- /usr/bin/wget
|
||||
- /bin/bash
|
||||
- /bin/sh
|
||||
- /usr/bin/python*
|
||||
@@ -0,0 +1,60 @@
|
||||
name: pypi-restrictive
|
||||
description: Restrictive sandbox policy for PyPI ecosystem (pip, poetry, uv)
|
||||
package_managers:
|
||||
- pip
|
||||
- pip3
|
||||
- poetry
|
||||
- uv
|
||||
|
||||
violation_mode: block
|
||||
|
||||
filesystem:
|
||||
allow_read:
|
||||
- ${CWD}/**
|
||||
- ${HOME}/.config/pip/**
|
||||
- ${HOME}/.pip/**
|
||||
- ${HOME}/.poetry/**
|
||||
- ${PM_CACHE}/**
|
||||
- /usr/local/**
|
||||
- /Library/**
|
||||
- /System/Library/**
|
||||
|
||||
allow_write:
|
||||
- ${CWD}/.venv/**
|
||||
- ${CWD}/venv/**
|
||||
- ${PM_CACHE}/**
|
||||
- ${HOME}/.local/lib/python*/**
|
||||
- ${TMPDIR}/**
|
||||
|
||||
deny_read:
|
||||
- ${HOME}/.ssh/**
|
||||
- ${HOME}/.aws/**
|
||||
- ${HOME}/.gcloud/**
|
||||
- "**/.env"
|
||||
|
||||
deny_write:
|
||||
- ${HOME}/.ssh/**
|
||||
- /etc/**
|
||||
- /usr/**
|
||||
|
||||
network:
|
||||
allow_outbound:
|
||||
- pypi.org:443
|
||||
- files.pythonhosted.org:443
|
||||
- github.com:443
|
||||
|
||||
deny_outbound:
|
||||
- "*:*"
|
||||
|
||||
process:
|
||||
allow_exec:
|
||||
- /usr/bin/python*
|
||||
- /usr/local/bin/python*
|
||||
- /usr/bin/gcc
|
||||
- /usr/bin/clang
|
||||
- /usr/bin/git
|
||||
|
||||
deny_exec:
|
||||
- /usr/bin/curl
|
||||
- /usr/bin/wget
|
||||
- /bin/bash
|
||||
@@ -0,0 +1,148 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed profiles/*.yml
|
||||
var profilesFS embed.FS
|
||||
|
||||
// defaultProfileRegistry implements ProfileRegistry with support for
|
||||
// built-in embedded profiles and custom user-provided profiles.
|
||||
type defaultProfileRegistry struct {
|
||||
mu sync.RWMutex
|
||||
profiles map[string]*SandboxPolicy
|
||||
}
|
||||
|
||||
// newDefaultProfileRegistry creates a new profile registry and loads built-in profiles.
|
||||
func newDefaultProfileRegistry() *defaultProfileRegistry {
|
||||
registry := &defaultProfileRegistry{
|
||||
profiles: make(map[string]*SandboxPolicy),
|
||||
}
|
||||
|
||||
// Load built-in profiles from embedded filesystem
|
||||
if err := registry.loadBuiltinProfiles(); err != nil {
|
||||
// Log error but don't fail - graceful degradation
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to load built-in sandbox profiles: %v\n", err)
|
||||
}
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
// loadBuiltinProfiles loads all built-in YAML profiles from the embedded filesystem.
|
||||
func (r *defaultProfileRegistry) loadBuiltinProfiles() error {
|
||||
entries, err := profilesFS.ReadDir("profiles")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read profiles directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yml") {
|
||||
continue
|
||||
}
|
||||
|
||||
profilePath := filepath.Join("profiles", entry.Name())
|
||||
data, err := profilesFS.ReadFile(profilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read profile %s: %w", entry.Name(), err)
|
||||
}
|
||||
|
||||
policy, err := parsePolicy(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse profile %s: %w", entry.Name(), err)
|
||||
}
|
||||
|
||||
if err := policy.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid profile %s: %w", entry.Name(), err)
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.profiles[policy.Name] = policy
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetProfile retrieves a policy by name.
|
||||
// First checks built-in profiles, then attempts to load as a custom file path.
|
||||
func (r *defaultProfileRegistry) GetProfile(name string) (*SandboxPolicy, error) {
|
||||
// Check if it's a built-in profile
|
||||
r.mu.RLock()
|
||||
if policy, exists := r.profiles[name]; exists {
|
||||
r.mu.RUnlock()
|
||||
return policy, nil
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
// Not a built-in profile - try to load as custom file
|
||||
if fileExists(name) {
|
||||
return r.LoadCustomProfile(name)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("sandbox profile not found: %s (not a built-in profile and file does not exist)", name)
|
||||
}
|
||||
|
||||
// LoadCustomProfile loads a policy from a custom YAML file path.
|
||||
func (r *defaultProfileRegistry) LoadCustomProfile(path string) (*SandboxPolicy, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read custom profile %s: %w", path, err)
|
||||
}
|
||||
|
||||
policy, err := parsePolicy(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse custom profile %s: %w", path, err)
|
||||
}
|
||||
|
||||
if err := policy.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid custom profile %s: %w", path, err)
|
||||
}
|
||||
|
||||
// Cache the custom profile for future use
|
||||
r.mu.Lock()
|
||||
r.profiles[policy.Name] = policy
|
||||
r.mu.Unlock()
|
||||
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
// ListProfiles returns the names of all built-in profiles.
|
||||
func (r *defaultProfileRegistry) ListProfiles() []string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
profiles := make([]string, 0, len(r.profiles))
|
||||
for name := range r.profiles {
|
||||
profiles = append(profiles, name)
|
||||
}
|
||||
|
||||
return profiles
|
||||
}
|
||||
|
||||
// parsePolicy parses a YAML policy file into a SandboxPolicy struct.
|
||||
func parsePolicy(data []byte) (*SandboxPolicy, error) {
|
||||
var policy SandboxPolicy
|
||||
|
||||
if err := yaml.Unmarshal(data, &policy); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse YAML: %w", err)
|
||||
}
|
||||
|
||||
return &policy, nil
|
||||
}
|
||||
|
||||
// fileExists checks if a file exists and is not a directory.
|
||||
func fileExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
// Name returns the sandbox implementation name (e.g., "seatbelt", "bubblewrap").
|
||||
Name() string
|
||||
|
||||
// IsAvailable returns true if the sandbox is available and functional on this platform.
|
||||
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 {
|
||||
// GetProfile retrieves a policy by name.
|
||||
// Name can be a built-in profile (e.g., "npm-restrictive") or a path to a custom YAML file.
|
||||
GetProfile(name string) (*SandboxPolicy, error)
|
||||
|
||||
// LoadCustomProfile loads a policy from a custom YAML file path.
|
||||
LoadCustomProfile(path string) (*SandboxPolicy, error)
|
||||
|
||||
// ListProfiles returns the names of all built-in profiles.
|
||||
ListProfiles() []string
|
||||
}
|
||||
|
||||
// NewProfileRegistry creates a new profile registry with built-in policies.
|
||||
func NewProfileRegistry() ProfileRegistry {
|
||||
return newDefaultProfileRegistry()
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//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()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package sandbox
|
||||
|
||||
import "errors"
|
||||
|
||||
// newPlatformSandbox creates a platform-specific sandbox instance for Linux.
|
||||
// Currently not implemented - returns an error.
|
||||
// Future implementations will use Bubblewrap or seccomp-bpf.
|
||||
func newPlatformSandbox() (Sandbox, error) {
|
||||
return nil, errors.New("sandbox not yet implemented for Linux (coming soon: Bubblewrap or seccomp-bpf)")
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//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")
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package sandbox
|
||||
|
||||
import "errors"
|
||||
|
||||
// newPlatformSandbox creates a platform-specific sandbox instance for Windows.
|
||||
// Currently not implemented - returns an error.
|
||||
// Future implementations will use AppContainer or Job Objects.
|
||||
func newPlatformSandbox() (Sandbox, error) {
|
||||
return nil, errors.New("sandbox not yet implemented for Windows (coming soon: AppContainer or Job Objects)")
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package seatbelt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/safedep/dry/log"
|
||||
)
|
||||
|
||||
// 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(),
|
||||
}, 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 {
|
||||
// Translate PMG policy to Seatbelt profile
|
||||
sbProfile, err := s.translator.Translate(policy)
|
||||
if err != nil {
|
||||
return 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)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
if _, err := tmpFile.WriteString(sbProfile); err != nil {
|
||||
tmpFile.Close()
|
||||
return fmt.Errorf("failed to write sandbox profile: %w", err)
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
log.Debugf("Seatbelt profile written to %s", tmpFile.Name())
|
||||
log.Debugf("Seatbelt profile content:\n%s", sbProfile)
|
||||
|
||||
// Modify command to run via sandbox-exec
|
||||
originalPath := cmd.Path
|
||||
originalArgs := cmd.Args
|
||||
|
||||
// sandbox-exec -f <profile> <command> <args...>
|
||||
cmd.Path = "/usr/bin/sandbox-exec"
|
||||
cmd.Args = []string{
|
||||
"sandbox-exec",
|
||||
"-f", tmpFile.Name(),
|
||||
originalPath,
|
||||
}
|
||||
|
||||
// Append original arguments (skip argv[0] which is the command itself)
|
||||
if len(originalArgs) > 1 {
|
||||
cmd.Args = append(cmd.Args, originalArgs[1:]...)
|
||||
}
|
||||
|
||||
log.Debugf("Sandboxed command: %s %v", cmd.Path, cmd.Args)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the name of this sandbox implementation.
|
||||
func (s *SeatbeltSandbox) Name() string {
|
||||
return "seatbelt"
|
||||
}
|
||||
|
||||
// IsAvailable returns true if sandbox-exec is available on this system.
|
||||
func (s *SeatbeltSandbox) IsAvailable() bool {
|
||||
_, err := exec.LookPath("sandbox-exec")
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package seatbelt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/safedep/pmg/sandbox/util"
|
||||
)
|
||||
|
||||
// PolicyTranslator translates PMG sandbox policies to Seatbelt Profile Language (.sb).
|
||||
type PolicyTranslator struct{}
|
||||
|
||||
// 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) {
|
||||
var sb strings.Builder
|
||||
|
||||
// Header
|
||||
sb.WriteString("(version 1)\n")
|
||||
sb.WriteString(fmt.Sprintf(";; PMG Sandbox Policy: %s\n", policy.Name))
|
||||
sb.WriteString(fmt.Sprintf(";; %s\n", policy.Description))
|
||||
sb.WriteString(";; Generated by PMG sandbox system\n\n")
|
||||
|
||||
// Default policy: deny by default for maximum security
|
||||
sb.WriteString("(deny default)\n\n")
|
||||
|
||||
// Allow basic system operations required for any process
|
||||
sb.WriteString(";; Basic system access\n")
|
||||
sb.WriteString("(allow process-fork)\n")
|
||||
sb.WriteString("(allow process-exec-interpreter)\n")
|
||||
sb.WriteString("(allow sysctl-read)\n")
|
||||
sb.WriteString("(allow mach-lookup)\n")
|
||||
sb.WriteString("(allow mach-register)\n")
|
||||
sb.WriteString("(allow ipc-posix-shm)\n")
|
||||
sb.WriteString("(allow signal)\n\n")
|
||||
|
||||
// Filesystem rules
|
||||
if err := t.translateFilesystem(policy, &sb); err != nil {
|
||||
return "", fmt.Errorf("failed to translate filesystem rules: %w", err)
|
||||
}
|
||||
|
||||
// Network rules
|
||||
if err := t.translateNetwork(policy, &sb); err != nil {
|
||||
return "", fmt.Errorf("failed to translate network rules: %w", err)
|
||||
}
|
||||
|
||||
// Process execution rules
|
||||
if err := t.translateProcess(policy, &sb); err != nil {
|
||||
return "", fmt.Errorf("failed to translate process rules: %w", err)
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// translateFilesystem translates filesystem access rules.
|
||||
func (t *PolicyTranslator) translateFilesystem(policy *SandboxPolicy, sb *strings.Builder) error {
|
||||
sb.WriteString(";; Filesystem access\n")
|
||||
|
||||
// Expand and add allow read rules
|
||||
for _, pattern := range policy.Filesystem.AllowRead {
|
||||
expanded, err := util.ExpandVariables(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to expand pattern %s: %w", pattern, err)
|
||||
}
|
||||
|
||||
// Handle glob patterns vs literal paths
|
||||
if util.ContainsGlob(expanded) {
|
||||
// For glob patterns, use subpath with the base directory
|
||||
baseDir := filepath.Dir(strings.TrimSuffix(expanded, "/**"))
|
||||
sb.WriteString(fmt.Sprintf("(allow file-read* (subpath \"%s\"))\n", baseDir))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("(allow file-read* (subpath \"%s\"))\n", expanded))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Expand and add allow write rules
|
||||
for _, pattern := range policy.Filesystem.AllowWrite {
|
||||
expanded, err := util.ExpandVariables(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to expand pattern %s: %w", pattern, err)
|
||||
}
|
||||
|
||||
if util.ContainsGlob(expanded) {
|
||||
baseDir := filepath.Dir(strings.TrimSuffix(expanded, "/**"))
|
||||
sb.WriteString(fmt.Sprintf("(allow file-write* (subpath \"%s\"))\n", baseDir))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("(allow file-write* (subpath \"%s\"))\n", expanded))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Deny rules have higher priority (applied after allow)
|
||||
// Note: Seatbelt evaluates rules in order, so denies after allows will override
|
||||
for _, pattern := range policy.Filesystem.DenyRead {
|
||||
expanded, err := util.ExpandVariables(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to expand pattern %s: %w", pattern, err)
|
||||
}
|
||||
|
||||
if util.ContainsGlob(expanded) {
|
||||
baseDir := filepath.Dir(strings.TrimSuffix(expanded, "/**"))
|
||||
sb.WriteString(fmt.Sprintf("(deny file-read* (subpath \"%s\"))\n", baseDir))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("(deny file-read* (subpath \"%s\"))\n", expanded))
|
||||
}
|
||||
}
|
||||
|
||||
for _, pattern := range policy.Filesystem.DenyWrite {
|
||||
expanded, err := util.ExpandVariables(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to expand pattern %s: %w", pattern, err)
|
||||
}
|
||||
|
||||
if util.ContainsGlob(expanded) {
|
||||
baseDir := filepath.Dir(strings.TrimSuffix(expanded, "/**"))
|
||||
sb.WriteString(fmt.Sprintf("(deny file-write* (subpath \"%s\"))\n", baseDir))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("(deny file-write* (subpath \"%s\"))\n", expanded))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// translateNetwork translates network access rules.
|
||||
func (t *PolicyTranslator) translateNetwork(policy *SandboxPolicy, sb *strings.Builder) error {
|
||||
sb.WriteString(";; Network access\n")
|
||||
|
||||
// If there are allow outbound rules, allow network-outbound generally
|
||||
// (Seatbelt doesn't support fine-grained host:port filtering in all cases)
|
||||
// Note: This is a limitation of Seatbelt - for more fine-grained control,
|
||||
// consider using a network filtering solution or firewall rules
|
||||
if len(policy.Network.AllowOutbound) > 0 {
|
||||
sb.WriteString(";; Network outbound allowed to specific hosts\n")
|
||||
sb.WriteString(";; Note: Seatbelt has limited host-based filtering, consider using firewall rules for strict control\n")
|
||||
sb.WriteString("(allow network-outbound)\n")
|
||||
}
|
||||
|
||||
// If deny outbound includes "*:*", block all network
|
||||
for _, pattern := range policy.Network.DenyOutbound {
|
||||
if pattern == "*:*" {
|
||||
sb.WriteString(";; Network outbound denied\n")
|
||||
sb.WriteString("(deny network-outbound)\n")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// translateProcess translates process execution rules.
|
||||
func (t *PolicyTranslator) translateProcess(policy *SandboxPolicy, sb *strings.Builder) error {
|
||||
sb.WriteString(";; Process execution\n")
|
||||
|
||||
// Add allow exec rules
|
||||
for _, exePath := range policy.Process.AllowExec {
|
||||
expanded, err := util.ExpandVariables(exePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to expand exec path %s: %w", exePath, err)
|
||||
}
|
||||
|
||||
if util.ContainsGlob(expanded) {
|
||||
// For glob patterns, use subpath to allow anything under that directory
|
||||
baseDir := filepath.Dir(strings.TrimSuffix(expanded, "/**"))
|
||||
sb.WriteString(fmt.Sprintf("(allow process-exec* (subpath \"%s\"))\n", baseDir))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("(allow process-exec* (literal \"%s\"))\n", expanded))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Add deny exec rules
|
||||
for _, exePath := range policy.Process.DenyExec {
|
||||
expanded, err := util.ExpandVariables(exePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to expand exec path %s: %w", exePath, err)
|
||||
}
|
||||
|
||||
if util.ContainsGlob(expanded) {
|
||||
baseDir := filepath.Dir(strings.TrimSuffix(expanded, "/**"))
|
||||
sb.WriteString(fmt.Sprintf("(deny process-exec* (subpath \"%s\"))\n", baseDir))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("(deny process-exec* (literal \"%s\"))\n", expanded))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExpandVariables expands known variables in a path or pattern.
|
||||
// Supported variables:
|
||||
// - ${HOME}: User home directory
|
||||
// - ${CWD}: Current working directory
|
||||
// - ${TMPDIR}: Temporary directory
|
||||
func ExpandVariables(pattern string) (string, error) {
|
||||
result := pattern
|
||||
|
||||
// Get home directory
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Get current working directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Get temp directory
|
||||
tmpDir := os.TempDir()
|
||||
|
||||
// Replace variables
|
||||
replacer := strings.NewReplacer(
|
||||
"${HOME}", home,
|
||||
"${CWD}", cwd,
|
||||
"${TMPDIR}", tmpDir,
|
||||
)
|
||||
|
||||
result = replacer.Replace(result)
|
||||
|
||||
// Clean up path (resolve .., ., etc.)
|
||||
result = filepath.Clean(result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ContainsGlob returns true if the pattern contains glob wildcards.
|
||||
func ContainsGlob(pattern string) bool {
|
||||
return strings.Contains(pattern, "*") ||
|
||||
strings.Contains(pattern, "?") ||
|
||||
strings.Contains(pattern, "[")
|
||||
}
|
||||
|
||||
// ExpandPathList expands variables in a list of paths/patterns.
|
||||
func ExpandPathList(patterns []string) ([]string, error) {
|
||||
result := make([]string, 0, len(patterns))
|
||||
|
||||
for _, pattern := range patterns {
|
||||
expanded, err := ExpandVariables(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, expanded)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package sandbox
|
||||
|
||||
import "github.com/safedep/pmg/sandbox/util"
|
||||
|
||||
// ExpandVariables expands known variables in a path or pattern.
|
||||
// This is a convenience wrapper around util.ExpandVariables.
|
||||
//
|
||||
// Supported variables:
|
||||
// - ${HOME}: User home directory
|
||||
// - ${CWD}: Current working directory
|
||||
// - ${TMPDIR}: Temporary directory
|
||||
func ExpandVariables(pattern string) (string, error) {
|
||||
return util.ExpandVariables(pattern)
|
||||
}
|
||||
|
||||
// ExpandPathList expands variables in a list of paths/patterns.
|
||||
// This is a convenience wrapper around util.ExpandPathList.
|
||||
func ExpandPathList(patterns []string) ([]string, error) {
|
||||
return util.ExpandPathList(patterns)
|
||||
}
|
||||
|
||||
// ContainsGlob returns true if the pattern contains glob wildcards.
|
||||
// This is a convenience wrapper around util.ContainsGlob.
|
||||
func ContainsGlob(pattern string) bool {
|
||||
return util.ContainsGlob(pattern)
|
||||
}
|
||||
Reference in New Issue
Block a user