feat: Add support for sandbox allow override (#165)

* feat: Add support for sandbox allow override

* fix: Main should fail on arg processing error

* fix: Remove redundant policy conflict check
This commit is contained in:
Abhisek Datta
2026-02-16 13:58:08 +05:30
committed by GitHub
parent d3cb15a3ea
commit 6074080219
9 changed files with 798 additions and 2 deletions
+27 -1
View File
@@ -1,6 +1,13 @@
package config
import "github.com/spf13/cobra"
import (
"fmt"
"github.com/spf13/cobra"
)
// sandboxAllowRaw holds the raw --sandbox-allow flag values before parsing.
var sandboxAllowRaw []string
// ApplyCobraFlags applies the cobra flags to the command.
// These flags are local concern of the config package. This helper function is used
@@ -29,7 +36,26 @@ func ApplyCobraFlags(cmd *cobra.Command) {
globalConfig.Config.Sandbox.EnforceAlways, "Apply sandbox to all commands, not just install commands (requires --sandbox)")
cmd.PersistentFlags().StringVar(&globalConfig.SandboxProfileOverride, "sandbox-profile",
globalConfig.SandboxProfileOverride, "Override sandbox policy profile (built-in name or path to custom YAML)")
cmd.PersistentFlags().StringArrayVar(&sandboxAllowRaw, "sandbox-allow",
nil, "Add runtime sandbox allow rule (type=value). Types: read, write, exec, net-connect, net-bind")
// Hide the experimental proxy mode flag but keep it for backward compatibility
_ = cmd.PersistentFlags().MarkHidden("experimental-proxy-mode")
}
// FinalizeSandboxAllowOverrides parses the raw --sandbox-allow flag values
// and stores the validated overrides in the global config. This must be called
// after cobra flag parsing is complete (e.g., in PersistentPreRun).
func FinalizeSandboxAllowOverrides() error {
if len(sandboxAllowRaw) == 0 {
return nil
}
overrides, err := parseSandboxAllowOverrides(sandboxAllowRaw)
if err != nil {
return fmt.Errorf("failed to parse --sandbox-allow flags: %w", err)
}
globalConfig.SandboxAllowOverrides = overrides
return nil
}
+28
View File
@@ -134,6 +134,11 @@ type RuntimeConfig struct {
// This is a CLI-only flag (--sandbox-profile) and is not persisted to config.yml.
SandboxProfileOverride string
// SandboxAllowOverrides holds runtime sandbox allow rules from --sandbox-allow flags.
// These are additive rules applied on top of the resolved sandbox policy.
// Not persisted to config.yml.
SandboxAllowOverrides []SandboxAllowOverride
// Internal config values computed at runtime and must be accessed via. API
configDir string
configFilePath string
@@ -161,6 +166,29 @@ func (r *RuntimeConfig) IsProxyModeEnabled() bool {
return (r.Config.ExperimentalProxyMode || r.Config.ProxyMode)
}
// SandboxAllowType represents the type of a sandbox allow override.
type SandboxAllowType string
const (
SandboxAllowRead SandboxAllowType = "read"
SandboxAllowWrite SandboxAllowType = "write"
SandboxAllowExec SandboxAllowType = "exec"
SandboxAllowNetConnect SandboxAllowType = "net-connect"
SandboxAllowNetBind SandboxAllowType = "net-bind"
)
// SandboxAllowOverride represents a single --sandbox-allow flag value.
type SandboxAllowOverride struct {
// Type is the resource type (read, write, exec, net-connect, net-bind).
Type SandboxAllowType
// Value is the resolved value (absolute path, host:port, etc.).
Value string
// Raw is the original CLI value before resolution (for logging/warnings).
Raw string
}
// DefaultConfig is a fail safe contract for the runtime configuration.
// The config package return an appropriate RuntimeConfig based on the environment and the configuration.
func DefaultConfig() RuntimeConfig {
+217
View File
@@ -0,0 +1,217 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/safedep/dry/log"
)
// validSandboxAllowTypes is the set of recognized --sandbox-allow type prefixes.
var validSandboxAllowTypes = map[SandboxAllowType]bool{
SandboxAllowRead: true,
SandboxAllowWrite: true,
SandboxAllowExec: true,
SandboxAllowNetConnect: true,
SandboxAllowNetBind: true,
}
// parseSandboxAllowOverrides parses raw --sandbox-allow flag values into validated overrides.
// Each raw value must be in the format "type=value" (e.g., "write=./.gitignore").
func parseSandboxAllowOverrides(raw []string) ([]SandboxAllowOverride, error) {
overrides := make([]SandboxAllowOverride, 0, len(raw))
for _, r := range raw {
override, err := parseSingleOverride(r)
if err != nil {
return nil, fmt.Errorf("invalid --sandbox-allow %q: %w", r, err)
}
overrides = append(overrides, override)
}
return overrides, nil
}
// parseSingleOverride parses and validates a single "type=value" string.
func parseSingleOverride(raw string) (SandboxAllowOverride, error) {
// Split on first '=' only to handle values containing '='
idx := strings.IndexByte(raw, '=')
if idx < 0 {
return SandboxAllowOverride{}, fmt.Errorf("missing '=' separator, expected format: type=value (e.g., write=./file)")
}
typStr := raw[:idx]
value := raw[idx+1:]
if typStr == "" {
return SandboxAllowOverride{}, fmt.Errorf("missing type before '=', expected format: type=value")
}
if value == "" {
return SandboxAllowOverride{}, fmt.Errorf("missing value after '=', expected format: type=value")
}
allowType := SandboxAllowType(typStr)
// Provide a helpful error for the common mistake of using "net" instead of "net-connect"/"net-bind"
if typStr == "net" {
return SandboxAllowOverride{}, fmt.Errorf("unknown type %q (use net-connect or net-bind)", typStr)
}
if !validSandboxAllowTypes[allowType] {
return SandboxAllowOverride{}, fmt.Errorf("unknown type %q, valid types: read, write, exec, net-connect, net-bind", typStr)
}
resolved, err := validateAndResolveValue(allowType, value)
if err != nil {
return SandboxAllowOverride{}, err
}
return SandboxAllowOverride{
Type: allowType,
Value: resolved,
Raw: raw,
}, nil
}
// validateAndResolveValue validates the value for the given type and resolves paths.
func validateAndResolveValue(typ SandboxAllowType, value string) (string, error) {
switch typ {
case SandboxAllowRead, SandboxAllowWrite:
return resolveFilesystemPath(value)
case SandboxAllowExec:
return resolveExecPath(value)
case SandboxAllowNetConnect:
return validateNetConnect(value)
case SandboxAllowNetBind:
return validateNetBind(value)
default:
return "", fmt.Errorf("unhandled type: %s", typ)
}
}
// resolveFilesystemPath resolves a filesystem path for read/write overrides.
// Supports glob patterns. Resolves relative paths to absolute via CWD.
func resolveFilesystemPath(value string) (string, error) {
if err := checkTildePath(value); err != nil {
return "", err
}
return resolveToAbsolute(value)
}
// resolveExecPath resolves an exec path. Rejects glob patterns.
func resolveExecPath(value string) (string, error) {
if err := checkTildePath(value); err != nil {
return "", err
}
if containsGlob(value) {
return "", fmt.Errorf("glob patterns are not allowed for exec type (specify exact path)")
}
return resolveToAbsolute(value)
}
// validateNetConnect validates a net-connect value (host:port format, no wildcards).
func validateNetConnect(value string) (string, error) {
host, port, err := parseHostPort(value)
if err != nil {
return "", fmt.Errorf("invalid net-connect value: %w", err)
}
if host == "*" || strings.Contains(host, "*") || strings.Contains(host, "?") {
return "", fmt.Errorf("wildcards are not allowed for net-connect (specify exact host:port)")
}
if port == "*" {
return "", fmt.Errorf("port wildcard is not allowed for net-connect (specify exact host:port)")
}
return value, nil
}
// validateNetBind validates a net-bind value.
// Allows localhost:* and 127.0.0.1:* as special wildcard forms.
// Warns on non-localhost addresses.
func validateNetBind(value string) (string, error) {
host, _, err := parseHostPort(value)
if err != nil {
return "", fmt.Errorf("invalid net-bind value: %w", err)
}
// Reject host-side wildcards
if host == "*" || strings.Contains(host, "*") || strings.Contains(host, "?") {
return "", fmt.Errorf("host wildcards are not allowed for net-bind (specify a host, e.g., localhost:3000)")
}
// Warn on non-localhost addresses
if !isLocalhostAddress(host) {
log.Warnf("--sandbox-allow net-bind=%s uses non-localhost address, this exposes the port to the network", value)
}
return value, nil
}
// parseHostPort parses a host:port string. The port may be "*" for wildcard.
func parseHostPort(value string) (string, string, error) {
// Find the last ':' to split host and port (handles IPv6 in the future)
lastColon := strings.LastIndex(value, ":")
if lastColon < 0 {
return "", "", fmt.Errorf("expected host:port format (e.g., example.com:443), got %q", value)
}
host := value[:lastColon]
port := value[lastColon+1:]
if host == "" {
return "", "", fmt.Errorf("missing host in host:port value %q", value)
}
if port == "" {
return "", "", fmt.Errorf("missing port in host:port value %q", value)
}
return host, port, nil
}
// isLocalhostAddress returns true if the host is a localhost address.
func isLocalhostAddress(host string) bool {
return host == "localhost" || host == "127.0.0.1" || host == "::1"
}
// resolveToAbsolute resolves a path to an absolute path relative to CWD.
// Glob characters are preserved. The path is cleaned via filepath.Clean().
func resolveToAbsolute(value string) (string, error) {
if filepath.IsAbs(value) {
return filepath.Clean(value), nil
}
// For paths with glob characters, we need to preserve them through Clean.
// filepath.Clean handles ".." and "." but leaves glob chars intact.
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("failed to get working directory: %w", err)
}
return filepath.Clean(filepath.Join(cwd, value)), nil
}
// checkTildePath returns an error if the path starts with "~" (unexpanded tilde).
func checkTildePath(value string) error {
if strings.HasPrefix(value, "~") {
return fmt.Errorf("path %q starts with '~' which was not expanded by your shell; use an absolute path instead", value)
}
return 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, "[")
}
+256
View File
@@ -0,0 +1,256 @@
package config
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseSandboxAllowOverrides_ValidFormats(t *testing.T) {
cwd, err := os.Getwd()
require.NoError(t, err)
tests := []struct {
name string
raw string
expectedType SandboxAllowType
expectedValue string
}{
{
name: "write with relative path",
raw: "write=./.gitignore",
expectedType: SandboxAllowWrite,
expectedValue: filepath.Join(cwd, ".gitignore"),
},
{
name: "write with absolute path",
raw: "write=/tmp/output",
expectedType: SandboxAllowWrite,
expectedValue: "/tmp/output",
},
{
name: "write with glob pattern",
raw: "write=./dist/**",
expectedType: SandboxAllowWrite,
expectedValue: filepath.Join(cwd, "dist/**"),
},
{
name: "read with absolute path",
raw: "read=/opt/config/registry.json",
expectedType: SandboxAllowRead,
expectedValue: "/opt/config/registry.json",
},
{
name: "read with glob pattern",
raw: "read=./src/**",
expectedType: SandboxAllowRead,
expectedValue: filepath.Join(cwd, "src/**"),
},
{
name: "exec with absolute path",
raw: "exec=/usr/bin/curl",
expectedType: SandboxAllowExec,
expectedValue: "/usr/bin/curl",
},
{
name: "net-connect with host:port",
raw: "net-connect=registry.npmjs.org:443",
expectedType: SandboxAllowNetConnect,
expectedValue: "registry.npmjs.org:443",
},
{
name: "net-bind with localhost",
raw: "net-bind=127.0.0.1:3000",
expectedType: SandboxAllowNetBind,
expectedValue: "127.0.0.1:3000",
},
{
name: "net-bind with localhost wildcard port",
raw: "net-bind=localhost:*",
expectedType: SandboxAllowNetBind,
expectedValue: "localhost:*",
},
{
name: "write with relative path no dot prefix",
raw: "write=dist/output",
expectedType: SandboxAllowWrite,
expectedValue: filepath.Join(cwd, "dist/output"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
overrides, err := parseSandboxAllowOverrides([]string{tt.raw})
require.NoError(t, err)
require.Len(t, overrides, 1)
assert.Equal(t, tt.expectedType, overrides[0].Type)
assert.Equal(t, tt.expectedValue, overrides[0].Value)
assert.Equal(t, tt.raw, overrides[0].Raw)
})
}
}
func TestParseSandboxAllowOverrides_MultipleValues(t *testing.T) {
raw := []string{
"write=./.gitignore",
"exec=/usr/bin/curl",
"net-connect=example.com:443",
}
overrides, err := parseSandboxAllowOverrides(raw)
require.NoError(t, err)
require.Len(t, overrides, 3)
assert.Equal(t, SandboxAllowWrite, overrides[0].Type)
assert.Equal(t, SandboxAllowExec, overrides[1].Type)
assert.Equal(t, SandboxAllowNetConnect, overrides[2].Type)
}
func TestParseSandboxAllowOverrides_EmptySlice(t *testing.T) {
overrides, err := parseSandboxAllowOverrides([]string{})
require.NoError(t, err)
assert.Empty(t, overrides)
}
func TestParseSandboxAllowOverrides_InvalidFormats(t *testing.T) {
tests := []struct {
name string
raw string
errContains string
}{
{
name: "missing separator",
raw: "./foo",
errContains: "missing '=' separator",
},
{
name: "missing type",
raw: "=./foo",
errContains: "missing type before '='",
},
{
name: "empty value",
raw: "write=",
errContains: "missing value after '='",
},
{
name: "unknown type",
raw: "foo=bar",
errContains: "unknown type",
},
{
name: "net shorthand rejected",
raw: "net=host:443",
errContains: "use net-connect or net-bind",
},
{
name: "exec with glob pattern",
raw: "exec=/usr/bin/*",
errContains: "glob patterns are not allowed for exec",
},
{
name: "net-connect with wildcard host",
raw: "net-connect=*:443",
errContains: "wildcards are not allowed for net-connect",
},
{
name: "net-connect with glob host",
raw: "net-connect=*.example.com:443",
errContains: "wildcards are not allowed for net-connect",
},
{
name: "net-connect with wildcard port",
raw: "net-connect=example.com:*",
errContains: "port wildcard is not allowed for net-connect",
},
{
name: "net-bind with host wildcard",
raw: "net-bind=*:3000",
errContains: "host wildcards are not allowed for net-bind",
},
{
name: "net-bind with full wildcard",
raw: "net-bind=*:*",
errContains: "host wildcards are not allowed for net-bind",
},
{
name: "net-connect missing port",
raw: "net-connect=example.com",
errContains: "expected host:port format",
},
{
name: "net-bind missing port",
raw: "net-bind=localhost",
errContains: "expected host:port format",
},
{
name: "tilde path for write",
raw: "write=~/file",
errContains: "starts with '~'",
},
{
name: "tilde path for read",
raw: "read=~/.config/foo",
errContains: "starts with '~'",
},
{
name: "tilde path for exec",
raw: "exec=~/bin/tool",
errContains: "starts with '~'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := parseSandboxAllowOverrides([]string{tt.raw})
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errContains)
})
}
}
func TestParseSandboxAllowOverrides_ValueWithEquals(t *testing.T) {
// Values can contain '=' characters — the parser splits on the first '=' only
overrides, err := parseSandboxAllowOverrides([]string{"write=./path=with=equals.txt"})
require.NoError(t, err)
require.Len(t, overrides, 1)
cwd, err := os.Getwd()
require.NoError(t, err)
assert.Equal(t, SandboxAllowWrite, overrides[0].Type)
assert.Equal(t, filepath.Join(cwd, "path=with=equals.txt"), overrides[0].Value)
}
func TestParseSandboxAllowOverrides_PathCleaning(t *testing.T) {
cwd, err := os.Getwd()
require.NoError(t, err)
// Paths with ".." are cleaned via filepath.Clean
overrides, err := parseSandboxAllowOverrides([]string{"write=./foo/../bar"})
require.NoError(t, err)
require.Len(t, overrides, 1)
assert.Equal(t, filepath.Join(cwd, "bar"), overrides[0].Value)
}
func TestParseSandboxAllowOverrides_NetBindNonLocalhost(t *testing.T) {
// Non-localhost should succeed (with a warning logged, which we can't easily assert here)
overrides, err := parseSandboxAllowOverrides([]string{"net-bind=0.0.0.0:3000"})
require.NoError(t, err)
require.Len(t, overrides, 1)
assert.Equal(t, SandboxAllowNetBind, overrides[0].Type)
assert.Equal(t, "0.0.0.0:3000", overrides[0].Value)
}
func TestParseSandboxAllowOverrides_FirstErrorStops(t *testing.T) {
// If the first value is invalid, the second is not parsed
_, err := parseSandboxAllowOverrides([]string{"write=./ok", "bad"})
require.Error(t, err)
assert.Contains(t, err.Error(), "missing '=' separator")
}
+29 -1
View File
@@ -54,6 +54,34 @@ Run sandbox with custom policy file:
pmg --sandbox --sandbox-profile=/path/to/custom-policy.yml npm install express
```
### Runtime Allow Overrides
Use `--sandbox-allow` to make one-off exceptions without creating a custom profile. This is useful when a command needs access that the default profile blocks.
```bash
# Allow writing to a specific file
pmg --sandbox-allow write=./.gitignore npx create-next-app@latest
# Allow executing a binary blocked by the profile
pmg --sandbox-allow exec=$(which curl) npm install some-package
# Allow outbound connection to a private registry
pmg --sandbox-allow net-connect=npm.internal.corp:443 npm install @corp/private-pkg
# Allow a dev server to bind to a local port
pmg --sandbox-allow net-bind=127.0.0.1:3000 npx some-dev-tool
# Multiple overrides
pmg \
--sandbox-allow write=./.gitignore \
--sandbox-allow exec=$(which curl) \
npm install some-package
```
Supported types: `read`, `write`, `exec`, `net-connect`, `net-bind`.
Overrides are additive (append to allow lists), non-persistent (apply to current invocation only), and logged in the event log for auditing. They cannot bypass explicit deny rules in the profile or mandatory security protections (`.env`, `.ssh`, `.aws`, `.git/hooks`, etc.).
<details>
<summary>Custom policy overrides using Policy Templates</summary>
@@ -256,4 +284,4 @@ bwrap --verbose [arguments...] -- npm install express
- https://github.com/anthropic-experimental/sandbox-runtime
- https://geminicli.com/docs/cli/sandbox/
- https://github.com/containers/bubblewrap
- https://github.com/containers/bubblewrap
+17
View File
@@ -25,6 +25,7 @@ const (
EventTypeDependencyResolved EventType = "dependency_resolved"
EventTypeInstallInsecureBypass EventType = "install_insecure_bypass"
EventTypeProxyHostObserved EventType = "proxy_host_observed"
EventTypeSandboxOverride EventType = "sandbox_override"
EventTypeError EventType = "error"
)
@@ -413,6 +414,22 @@ func LogProxyHostObserved(hostname, method, reason string, details map[string]in
}
}
// LogSandboxOverrides logs when runtime sandbox allow overrides are applied.
func LogSandboxOverrides(sandboxProfile string, overrides []map[string]string) {
event := Event{
EventType: EventTypeSandboxOverride,
Message: fmt.Sprintf("Sandbox runtime overrides applied (%d rules)", len(overrides)),
Details: map[string]interface{}{
"sandbox_profile": sandboxProfile,
"sandbox_runtime_overrides": overrides,
},
}
if err := LogEvent(event); err != nil {
log.Warnf("failed to log sandbox override event: %s", err)
}
}
// LogError logs an error event
func LogError(message string, err error) {
event := Event{
+5
View File
@@ -72,6 +72,11 @@ func main() {
if eventlogErr != nil {
ui.Fatalf("failed to initialize event logging: %v", eventlogErr)
}
// Parse and validate --sandbox-allow flags after all flags are resolved
if err := config.FinalizeSandboxAllowOverrides(); err != nil {
ui.Fatalf("pmg: %v", err)
}
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
+54
View File
@@ -7,7 +7,9 @@ import (
"path/filepath"
"github.com/safedep/dry/log"
"github.com/safedep/dry/utils"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/internal/eventlog"
"github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/sandbox/platform"
"github.com/safedep/pmg/usefulerror"
@@ -114,6 +116,12 @@ func ApplySandbox(ctx context.Context, cmd *exec.Cmd, pmName string, opts ...app
log.Debugf("Loaded sandbox policy %s", policy.Name)
// Apply runtime --sandbox-allow overrides to the policy before execution
if len(cfg.SandboxAllowOverrides) > 0 {
applyRuntimeOverrides(policy, cfg.SandboxAllowOverrides)
logSandboxOverridesToEventLog(policy.Name, cfg.SandboxAllowOverrides)
}
if !policy.AppliesToPackageManager(pmName) {
return nil, fmt.Errorf("sandbox policy %s does not apply to %s", policy.Name, pmName)
}
@@ -146,3 +154,49 @@ func ApplySandbox(ctx context.Context, cmd *exec.Cmd, pmName string, opts ...app
return result, nil
}
// applyRuntimeOverrides applies --sandbox-allow overrides to the policy.
// Overrides are additive — they only append to allow lists, never modify deny lists.
// Warnings are logged for conflicts with deny rules and mandatory deny patterns.
func applyRuntimeOverrides(policy *sandbox.SandboxPolicy, overrides []config.SandboxAllowOverride) {
for _, override := range overrides {
switch override.Type {
case config.SandboxAllowRead:
log.Infof("Sandbox override: allowing read access to %s", override.Value)
policy.Filesystem.AllowRead = append(policy.Filesystem.AllowRead, override.Value)
case config.SandboxAllowWrite:
log.Infof("Sandbox override: allowing write access to %s", override.Value)
policy.Filesystem.AllowWrite = append(policy.Filesystem.AllowWrite, override.Value)
case config.SandboxAllowExec:
log.Infof("Sandbox override: allowing execution of %s", override.Value)
policy.Process.AllowExec = append(policy.Process.AllowExec, override.Value)
case config.SandboxAllowNetConnect:
log.Infof("Sandbox override: allowing outbound connection to %s", override.Value)
policy.Network.AllowOutbound = append(policy.Network.AllowOutbound, override.Value)
case config.SandboxAllowNetBind:
log.Infof("Sandbox override: allowing network bind on %s", override.Value)
policy.Network.AllowBind = append(policy.Network.AllowBind, override.Value)
// Enable AllowNetworkBind so the translator emits bind rules.
// Without this, AllowBind entries would be ignored on some platforms.
policy.AllowNetworkBind = utils.PtrTo(true)
}
}
}
// logSandboxOverridesToEventLog records sandbox allow overrides in the audit event log.
func logSandboxOverridesToEventLog(profileName string, overrides []config.SandboxAllowOverride) {
entries := make([]map[string]string, 0, len(overrides))
for _, o := range overrides {
entries = append(entries, map[string]string{
"type": string(o.Type),
"value": o.Value,
})
}
eventlog.LogSandboxOverrides(profileName, entries)
}
+165
View File
@@ -0,0 +1,165 @@
package executor
import (
"testing"
"github.com/safedep/dry/utils"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/sandbox"
"github.com/stretchr/testify/assert"
)
func TestApplyRuntimeOverrides_Read(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Filesystem: sandbox.FilesystemPolicy{
AllowRead: []string{"/existing"},
},
}
applyRuntimeOverrides(policy, []config.SandboxAllowOverride{
{Type: config.SandboxAllowRead, Value: "/new/path", Raw: "read=/new/path"},
})
assert.Contains(t, policy.Filesystem.AllowRead, "/existing")
assert.Contains(t, policy.Filesystem.AllowRead, "/new/path")
}
func TestApplyRuntimeOverrides_Write(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Filesystem: sandbox.FilesystemPolicy{
AllowWrite: []string{"/existing"},
},
}
applyRuntimeOverrides(policy, []config.SandboxAllowOverride{
{Type: config.SandboxAllowWrite, Value: "/new/file", Raw: "write=/new/file"},
})
assert.Contains(t, policy.Filesystem.AllowWrite, "/existing")
assert.Contains(t, policy.Filesystem.AllowWrite, "/new/file")
}
func TestApplyRuntimeOverrides_Exec(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Process: sandbox.ProcessPolicy{
AllowExec: []string{"/usr/bin/node"},
},
}
applyRuntimeOverrides(policy, []config.SandboxAllowOverride{
{Type: config.SandboxAllowExec, Value: "/usr/bin/curl", Raw: "exec=/usr/bin/curl"},
})
assert.Contains(t, policy.Process.AllowExec, "/usr/bin/node")
assert.Contains(t, policy.Process.AllowExec, "/usr/bin/curl")
}
func TestApplyRuntimeOverrides_NetConnect(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Network: sandbox.NetworkPolicy{
AllowOutbound: []string{"registry.npmjs.org:443"},
},
}
applyRuntimeOverrides(policy, []config.SandboxAllowOverride{
{Type: config.SandboxAllowNetConnect, Value: "example.com:443", Raw: "net-connect=example.com:443"},
})
assert.Contains(t, policy.Network.AllowOutbound, "registry.npmjs.org:443")
assert.Contains(t, policy.Network.AllowOutbound, "example.com:443")
}
func TestApplyRuntimeOverrides_NetBind(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Network: sandbox.NetworkPolicy{
AllowBind: []string{},
},
}
applyRuntimeOverrides(policy, []config.SandboxAllowOverride{
{Type: config.SandboxAllowNetBind, Value: "127.0.0.1:3000", Raw: "net-bind=127.0.0.1:3000"},
})
assert.Contains(t, policy.Network.AllowBind, "127.0.0.1:3000")
assert.NotNil(t, policy.AllowNetworkBind)
assert.True(t, *policy.AllowNetworkBind)
}
func TestApplyRuntimeOverrides_NetBindPreservesExistingTrue(t *testing.T) {
policy := &sandbox.SandboxPolicy{
AllowNetworkBind: utils.PtrTo(true),
Network: sandbox.NetworkPolicy{
AllowBind: []string{"localhost:8080"},
},
}
applyRuntimeOverrides(policy, []config.SandboxAllowOverride{
{Type: config.SandboxAllowNetBind, Value: "127.0.0.1:3000", Raw: "net-bind=127.0.0.1:3000"},
})
assert.Contains(t, policy.Network.AllowBind, "localhost:8080")
assert.Contains(t, policy.Network.AllowBind, "127.0.0.1:3000")
assert.True(t, *policy.AllowNetworkBind)
}
func TestApplyRuntimeOverrides_MultipleOverrides(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Filesystem: sandbox.FilesystemPolicy{},
Process: sandbox.ProcessPolicy{},
Network: sandbox.NetworkPolicy{},
}
overrides := []config.SandboxAllowOverride{
{Type: config.SandboxAllowWrite, Value: "/path/a", Raw: "write=/path/a"},
{Type: config.SandboxAllowWrite, Value: "/path/b", Raw: "write=/path/b"},
{Type: config.SandboxAllowExec, Value: "/usr/bin/curl", Raw: "exec=/usr/bin/curl"},
{Type: config.SandboxAllowNetConnect, Value: "example.com:443", Raw: "net-connect=example.com:443"},
}
applyRuntimeOverrides(policy, overrides)
assert.Len(t, policy.Filesystem.AllowWrite, 2)
assert.Len(t, policy.Process.AllowExec, 1)
assert.Len(t, policy.Network.AllowOutbound, 1)
}
func TestApplyRuntimeOverrides_EmptyOverrides(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Filesystem: sandbox.FilesystemPolicy{
AllowWrite: []string{"/existing"},
},
}
applyRuntimeOverrides(policy, []config.SandboxAllowOverride{})
// Policy should be unchanged
assert.Equal(t, []string{"/existing"}, policy.Filesystem.AllowWrite)
}
func TestApplyRuntimeOverrides_DenyListsUnmodified(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Filesystem: sandbox.FilesystemPolicy{
DenyWrite: []string{"/protected"},
},
Process: sandbox.ProcessPolicy{
DenyExec: []string{"/usr/bin/curl"},
},
Network: sandbox.NetworkPolicy{
DenyOutbound: []string{"*:*"},
},
}
overrides := []config.SandboxAllowOverride{
{Type: config.SandboxAllowWrite, Value: "/something", Raw: "write=/something"},
{Type: config.SandboxAllowExec, Value: "/usr/bin/wget", Raw: "exec=/usr/bin/wget"},
{Type: config.SandboxAllowNetConnect, Value: "example.com:443", Raw: "net-connect=example.com:443"},
}
applyRuntimeOverrides(policy, overrides)
// Deny lists should never be modified by overrides
assert.Equal(t, []string{"/protected"}, policy.Filesystem.DenyWrite)
assert.Equal(t, []string{"/usr/bin/curl"}, policy.Process.DenyExec)
assert.Equal(t, []string{"*:*"}, policy.Network.DenyOutbound)
}