This repository has been archived on 2026-03-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
greywall/internal/sandbox/macos_test.go
Mathieu Virbel da3a2ac3a4 rename Fence to Greywall as GreyHaven sandboxing component
Rebrand the project from Fence to Greywall, the sandboxing layer of the
GreyHaven platform. This updates:

- Go module path to gitea.app.monadical.io/monadical/greywall
- Binary name, CLI help text, and all usage examples
- Config paths (~/.config/greywall/greywall.json), env vars (GREYWALL_*)
- Log prefixes ([greywall:*]), temp file prefixes (greywall-*)
- All documentation, scripts, CI workflows, and example files
- README rewritten with GreyHaven branding and Fence attribution

Directory/file renames: cmd/fence → cmd/greywall, pkg/fence → pkg/greywall,
docs/why-fence.md → docs/why-greywall.md, example JSON files, and banner.
2026-02-10 16:00:24 -06:00

315 lines
9.2 KiB
Go

package sandbox
import (
"strings"
"testing"
"gitea.app.monadical.io/monadical/greywall/internal/config"
)
// TestMacOS_NetworkRestrictionWithProxy verifies that when a proxy URL is set,
// the macOS sandbox profile allows outbound to the proxy host:port.
func TestMacOS_NetworkRestrictionWithProxy(t *testing.T) {
tests := []struct {
name string
proxyURL string
wantProxy bool
proxyHost string
proxyPort string
}{
{
name: "no proxy - network blocked",
proxyURL: "",
wantProxy: false,
},
{
name: "socks5 proxy - outbound allowed to proxy",
proxyURL: "socks5://proxy.example.com:1080",
wantProxy: true,
proxyHost: "proxy.example.com",
proxyPort: "1080",
},
{
name: "socks5h proxy - outbound allowed to proxy",
proxyURL: "socks5h://localhost:1080",
wantProxy: true,
proxyHost: "localhost",
proxyPort: "1080",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &config.Config{
Network: config.NetworkConfig{
ProxyURL: tt.proxyURL,
},
Filesystem: config.FilesystemConfig{
AllowWrite: []string{"/tmp/test"},
},
}
params := buildMacOSParamsForTest(cfg)
if tt.wantProxy {
if params.ProxyHost != tt.proxyHost {
t.Errorf("expected ProxyHost %q, got %q", tt.proxyHost, params.ProxyHost)
}
if params.ProxyPort != tt.proxyPort {
t.Errorf("expected ProxyPort %q, got %q", tt.proxyPort, params.ProxyPort)
}
profile := GenerateSandboxProfile(params)
expectedRule := `(allow network-outbound (remote ip "` + tt.proxyHost + ":" + tt.proxyPort + `"))`
if !strings.Contains(profile, expectedRule) {
t.Errorf("profile should contain proxy outbound rule %q", expectedRule)
}
}
// Network should always be restricted (proxy or not)
if !params.NeedsNetworkRestriction {
t.Error("NeedsNetworkRestriction should always be true")
}
})
}
}
// buildMacOSParamsForTest is a helper to build MacOSSandboxParams from config,
// replicating the logic in WrapCommandMacOS for testing.
func buildMacOSParamsForTest(cfg *config.Config) MacOSSandboxParams {
allowPaths := append(GetDefaultWritePaths(), cfg.Filesystem.AllowWrite...)
allowLocalBinding := cfg.Network.AllowLocalBinding
allowLocalOutbound := allowLocalBinding
if cfg.Network.AllowLocalOutbound != nil {
allowLocalOutbound = *cfg.Network.AllowLocalOutbound
}
var proxyHost, proxyPort string
if cfg.Network.ProxyURL != "" {
// Simple parsing for tests
parts := strings.SplitN(cfg.Network.ProxyURL, "://", 2)
if len(parts) == 2 {
hostPort := parts[1]
colonIdx := strings.LastIndex(hostPort, ":")
if colonIdx >= 0 {
proxyHost = hostPort[:colonIdx]
proxyPort = hostPort[colonIdx+1:]
}
}
}
return MacOSSandboxParams{
Command: "echo test",
NeedsNetworkRestriction: true,
ProxyURL: cfg.Network.ProxyURL,
ProxyHost: proxyHost,
ProxyPort: proxyPort,
AllowUnixSockets: cfg.Network.AllowUnixSockets,
AllowAllUnixSockets: cfg.Network.AllowAllUnixSockets,
AllowLocalBinding: allowLocalBinding,
AllowLocalOutbound: allowLocalOutbound,
DefaultDenyRead: cfg.Filesystem.DefaultDenyRead,
ReadAllowPaths: cfg.Filesystem.AllowRead,
ReadDenyPaths: cfg.Filesystem.DenyRead,
WriteAllowPaths: allowPaths,
WriteDenyPaths: cfg.Filesystem.DenyWrite,
AllowPty: cfg.AllowPty,
AllowGitConfig: cfg.Filesystem.AllowGitConfig,
}
}
// TestMacOS_ProfileNetworkSection verifies the network section of generated profiles.
func TestMacOS_ProfileNetworkSection(t *testing.T) {
tests := []struct {
name string
restricted bool
wantContains []string
wantNotContain []string
}{
{
name: "unrestricted network allows all",
restricted: false,
wantContains: []string{
"(allow network*)", // Blanket allow all network operations
},
wantNotContain: []string{},
},
{
name: "restricted network does not allow all",
restricted: true,
wantContains: []string{
"; Network", // Should have network section
},
wantNotContain: []string{
"(allow network*)", // Should NOT have blanket allow
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
params := MacOSSandboxParams{
Command: "echo test",
NeedsNetworkRestriction: tt.restricted,
}
profile := GenerateSandboxProfile(params)
for _, want := range tt.wantContains {
if !strings.Contains(profile, want) {
t.Errorf("profile should contain %q, got:\n%s", want, profile)
}
}
for _, notWant := range tt.wantNotContain {
if strings.Contains(profile, notWant) {
t.Errorf("profile should NOT contain %q", notWant)
}
}
})
}
}
// TestMacOS_DefaultDenyRead verifies that the defaultDenyRead option properly restricts filesystem reads.
func TestMacOS_DefaultDenyRead(t *testing.T) {
tests := []struct {
name string
defaultDenyRead bool
allowRead []string
wantContainsBlanketAllow bool
wantContainsMetadataAllow bool
wantContainsSystemAllows bool
wantContainsUserAllowRead bool
}{
{
name: "default mode - blanket allow read",
defaultDenyRead: false,
allowRead: nil,
wantContainsBlanketAllow: true,
wantContainsMetadataAllow: false,
wantContainsSystemAllows: false,
wantContainsUserAllowRead: false,
},
{
name: "defaultDenyRead enabled - metadata allow, system data allows",
defaultDenyRead: true,
allowRead: nil,
wantContainsBlanketAllow: false,
wantContainsMetadataAllow: true,
wantContainsSystemAllows: true,
wantContainsUserAllowRead: false,
},
{
name: "defaultDenyRead with allowRead paths",
defaultDenyRead: true,
allowRead: []string{"/home/user/project"},
wantContainsBlanketAllow: false,
wantContainsMetadataAllow: true,
wantContainsSystemAllows: true,
wantContainsUserAllowRead: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
params := MacOSSandboxParams{
Command: "echo test",
DefaultDenyRead: tt.defaultDenyRead,
ReadAllowPaths: tt.allowRead,
}
profile := GenerateSandboxProfile(params)
hasBlanketAllow := strings.Contains(profile, "(allow file-read*)\n")
if hasBlanketAllow != tt.wantContainsBlanketAllow {
t.Errorf("blanket file-read allow = %v, want %v", hasBlanketAllow, tt.wantContainsBlanketAllow)
}
hasMetadataAllow := strings.Contains(profile, "(allow file-read-metadata)")
if hasMetadataAllow != tt.wantContainsMetadataAllow {
t.Errorf("file-read-metadata allow = %v, want %v", hasMetadataAllow, tt.wantContainsMetadataAllow)
}
hasSystemAllows := strings.Contains(profile, `(subpath "/usr")`) ||
strings.Contains(profile, `(subpath "/bin")`)
if hasSystemAllows != tt.wantContainsSystemAllows {
t.Errorf("system path allows = %v, want %v\nProfile:\n%s", hasSystemAllows, tt.wantContainsSystemAllows, profile)
}
if tt.wantContainsUserAllowRead && len(tt.allowRead) > 0 {
hasUserAllow := strings.Contains(profile, tt.allowRead[0])
if !hasUserAllow {
t.Errorf("user allowRead path %q not found in profile", tt.allowRead[0])
}
}
})
}
}
// TestExpandMacOSTmpPaths verifies that /tmp and /private/tmp paths are properly mirrored.
func TestExpandMacOSTmpPaths(t *testing.T) {
tests := []struct {
name string
input []string
want []string
}{
{
name: "mirrors /tmp to /private/tmp",
input: []string{".", "/tmp"},
want: []string{".", "/tmp", "/private/tmp"},
},
{
name: "mirrors /private/tmp to /tmp",
input: []string{".", "/private/tmp"},
want: []string{".", "/private/tmp", "/tmp"},
},
{
name: "no change when both present",
input: []string{".", "/tmp", "/private/tmp"},
want: []string{".", "/tmp", "/private/tmp"},
},
{
name: "no change when neither present",
input: []string{".", "~/.cache"},
want: []string{".", "~/.cache"},
},
{
name: "mirrors /tmp/greywall to /private/tmp/greywall",
input: []string{".", "/tmp/greywall"},
want: []string{".", "/tmp/greywall", "/private/tmp/greywall"},
},
{
name: "mirrors /private/tmp/greywall to /tmp/greywall",
input: []string{".", "/private/tmp/greywall"},
want: []string{".", "/private/tmp/greywall", "/tmp/greywall"},
},
{
name: "mirrors nested subdirectory",
input: []string{".", "/tmp/foo/bar"},
want: []string{".", "/tmp/foo/bar", "/private/tmp/foo/bar"},
},
{
name: "no duplicate when mirror already present",
input: []string{".", "/tmp/greywall", "/private/tmp/greywall"},
want: []string{".", "/tmp/greywall", "/private/tmp/greywall"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := expandMacOSTmpPaths(tt.input)
if len(got) != len(tt.want) {
t.Errorf("expandMacOSTmpPaths() = %v, want %v", got, tt.want)
return
}
for i, v := range got {
if v != tt.want[i] {
t.Errorf("expandMacOSTmpPaths()[%d] = %v, want %v", i, v, tt.want[i])
}
}
})
}
}