mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
fix: Fix sandbox policy generator for MacOS min permissions
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/safedep/pmg/sandbox"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSeatbeltTranslatorDarwinCommonTranslation(t *testing.T) {
|
||||
policy := &sandbox.SandboxPolicy{
|
||||
Name: "test",
|
||||
Description: "test",
|
||||
PackageManagers: []string{"npm"},
|
||||
Filesystem: sandbox.FilesystemPolicy{
|
||||
AllowRead: []string{"/tmp"},
|
||||
AllowWrite: []string{"/tmp"},
|
||||
DenyRead: []string{"/private/var"},
|
||||
DenyWrite: []string{"/private/var"},
|
||||
},
|
||||
Network: sandbox.NetworkPolicy{
|
||||
AllowOutbound: []string{"*:*"},
|
||||
},
|
||||
Process: sandbox.ProcessPolicy{
|
||||
AllowExec: []string{"/bin/sh"},
|
||||
DenyExec: []string{"/bin/bash"},
|
||||
},
|
||||
}
|
||||
|
||||
translator := newSeatbeltPolicyTranslator()
|
||||
actual, err := translator.translate(policy)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test common translation
|
||||
assert.Contains(t, actual, "(version 1)")
|
||||
assert.Contains(t, actual, fmt.Sprintf(";; PMG Sandbox Policy: %s", policy.Name))
|
||||
assert.Contains(t, actual, fmt.Sprintf(";; %s", policy.Description))
|
||||
assert.Contains(t, actual, ";; Generated by PMG sandbox system")
|
||||
|
||||
// Test deny default - it should contain a message tag
|
||||
assert.Contains(t, actual, "(deny default (with message")
|
||||
|
||||
// Allow reading /dev and /etc
|
||||
assert.Contains(t, actual, "(allow file-read* (subpath \"/dev\"))")
|
||||
assert.Contains(t, actual, "(allow file-read* (subpath \"/etc\"))")
|
||||
|
||||
// Allow process fork
|
||||
assert.Contains(t, actual, "(allow process-fork)")
|
||||
}
|
||||
|
||||
func TestSeatbeltTranslatorDarwinFilesystemTranslation(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
policy *sandbox.SandboxPolicy
|
||||
assert func(t *testing.T, actual string, err error)
|
||||
}{
|
||||
{
|
||||
name: "simple path",
|
||||
policy: &sandbox.SandboxPolicy{
|
||||
Filesystem: sandbox.FilesystemPolicy{
|
||||
AllowRead: []string{"/tmp"},
|
||||
AllowWrite: []string{"/tmp"},
|
||||
DenyRead: []string{"/private/var"},
|
||||
DenyWrite: []string{"/private/var"},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, actual string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, actual, "(allow file-read* (subpath \"/tmp\"))")
|
||||
assert.Contains(t, actual, "(allow file-write* (subpath \"/tmp\"))")
|
||||
assert.Contains(t, actual, "(deny file-read* (subpath \"/private/var\") (with message")
|
||||
assert.Contains(t, actual, "(deny file-write* (subpath \"/private/var\") (with message")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "glob pattern with /**",
|
||||
policy: &sandbox.SandboxPolicy{
|
||||
Filesystem: sandbox.FilesystemPolicy{
|
||||
AllowRead: []string{"/path/to/dir/**"},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, actual string, err error) {
|
||||
assert.NoError(t, err)
|
||||
// Should use regex matching for glob patterns
|
||||
assert.Contains(t, actual, "(allow file-read* (regex")
|
||||
assert.Contains(t, actual, "^/path/to/dir/.*$")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "glob pattern with *.txt",
|
||||
policy: &sandbox.SandboxPolicy{
|
||||
Filesystem: sandbox.FilesystemPolicy{
|
||||
AllowRead: []string{"/path/to/*.txt"},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, actual string, err error) {
|
||||
assert.NoError(t, err)
|
||||
// Should use regex matching for glob patterns
|
||||
assert.Contains(t, actual, "(allow file-read* (regex")
|
||||
assert.Contains(t, actual, `^/path/to/[^/]*\.txt$`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "glob pattern with ? wildcard",
|
||||
policy: &sandbox.SandboxPolicy{
|
||||
Filesystem: sandbox.FilesystemPolicy{
|
||||
AllowWrite: []string{"/path/to/file?.log"},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, actual string, err error) {
|
||||
assert.NoError(t, err)
|
||||
// Should use regex matching for glob patterns
|
||||
assert.Contains(t, actual, "(allow file-write* (regex")
|
||||
assert.Contains(t, actual, `^/path/to/file[^/]\.log$`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "glob pattern in middle of path",
|
||||
policy: &sandbox.SandboxPolicy{
|
||||
Filesystem: sandbox.FilesystemPolicy{
|
||||
DenyRead: []string{"/path/*/subdir"},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, actual string, err error) {
|
||||
assert.NoError(t, err)
|
||||
// Should use regex matching for glob patterns
|
||||
assert.Contains(t, actual, "(deny file-read* (regex")
|
||||
assert.Contains(t, actual, `^/path/[^/]*/subdir$`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "glob pattern with bracket wildcard",
|
||||
policy: &sandbox.SandboxPolicy{
|
||||
Filesystem: sandbox.FilesystemPolicy{
|
||||
AllowRead: []string{"/tmp/test[123].txt"},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, actual string, err error) {
|
||||
assert.NoError(t, err)
|
||||
// Should use regex matching for glob patterns
|
||||
assert.Contains(t, actual, "(allow file-read* (regex")
|
||||
assert.Contains(t, actual, `^/tmp/test[123]\.txt$`)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
translator := newSeatbeltPolicyTranslator()
|
||||
actual, err := translator.translate(tt.policy)
|
||||
tt.assert(t, actual, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeatbeltTranslatorDarwinProcessTranslation(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
policy *sandbox.SandboxPolicy
|
||||
assert func(t *testing.T, actual string, err error)
|
||||
}{
|
||||
{
|
||||
name: "literal exec path",
|
||||
policy: &sandbox.SandboxPolicy{
|
||||
Process: sandbox.ProcessPolicy{
|
||||
AllowExec: []string{"/bin/sh"},
|
||||
DenyExec: []string{"/bin/bash"},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, actual string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, actual, "(allow process-exec* (literal \"/bin/sh\"))")
|
||||
assert.Contains(t, actual, "(deny process-exec* (literal \"/bin/bash\") (with message")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "glob pattern with /** for exec",
|
||||
policy: &sandbox.SandboxPolicy{
|
||||
Process: sandbox.ProcessPolicy{
|
||||
AllowExec: []string{"/usr/local/bin/**"},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, actual string, err error) {
|
||||
assert.NoError(t, err)
|
||||
// Should use regex matching for glob patterns
|
||||
assert.Contains(t, actual, "(allow process-exec* (regex")
|
||||
assert.Contains(t, actual, "^/usr/local/bin/.*$")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "glob pattern with * wildcard for exec",
|
||||
policy: &sandbox.SandboxPolicy{
|
||||
Process: sandbox.ProcessPolicy{
|
||||
AllowExec: []string{"/usr/bin/python*"},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, actual string, err error) {
|
||||
assert.NoError(t, err)
|
||||
// Should use regex matching for glob patterns
|
||||
assert.Contains(t, actual, "(allow process-exec* (regex")
|
||||
assert.Contains(t, actual, `^/usr/bin/python[^/]*$`)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
translator := newSeatbeltPolicyTranslator()
|
||||
actual, err := translator.translate(tt.policy)
|
||||
tt.assert(t, actual, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractBaseDir(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
pattern string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no glob pattern",
|
||||
pattern: "/path/to/file",
|
||||
expected: "/path/to/file",
|
||||
},
|
||||
{
|
||||
name: "glob pattern with /**",
|
||||
pattern: "/path/to/**",
|
||||
expected: "/path/to",
|
||||
},
|
||||
{
|
||||
name: "glob pattern with *.txt",
|
||||
pattern: "/path/to/*.txt",
|
||||
expected: "/path/to",
|
||||
},
|
||||
{
|
||||
name: "glob pattern with ? wildcard",
|
||||
pattern: "/path/to/file?.log",
|
||||
expected: "/path/to",
|
||||
},
|
||||
{
|
||||
name: "glob pattern in middle of path",
|
||||
pattern: "/path/*/subdir",
|
||||
expected: "/path",
|
||||
},
|
||||
{
|
||||
name: "glob pattern with bracket wildcard",
|
||||
pattern: "/tmp/test[123].txt",
|
||||
expected: "/tmp",
|
||||
},
|
||||
{
|
||||
name: "glob pattern at root",
|
||||
pattern: "/*.txt",
|
||||
expected: "/",
|
||||
},
|
||||
{
|
||||
name: "multiple glob patterns",
|
||||
pattern: "/path/*/sub/*.txt",
|
||||
expected: "/path",
|
||||
},
|
||||
{
|
||||
name: "complex glob with multiple wildcards",
|
||||
pattern: "/usr/bin/python*",
|
||||
expected: "/usr/bin",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := extractBaseDir(tt.pattern)
|
||||
assert.Equal(t, tt.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAncestorDirectories(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
path string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "deep path",
|
||||
path: "/private/tmp/test/file.txt",
|
||||
expected: []string{"/private/tmp/test", "/private/tmp", "/private"},
|
||||
},
|
||||
{
|
||||
name: "two level path",
|
||||
path: "/tmp/file.txt",
|
||||
expected: []string{"/tmp"},
|
||||
},
|
||||
{
|
||||
name: "root level path",
|
||||
path: "/file.txt",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "directory path",
|
||||
path: "/usr/local/bin",
|
||||
expected: []string{"/usr/local", "/usr"},
|
||||
},
|
||||
{
|
||||
name: "path with trailing slash",
|
||||
path: "/path/to/dir/",
|
||||
expected: []string{"/path/to/dir", "/path/to", "/path"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := getAncestorDirectories(tt.path)
|
||||
assert.Equal(t, tt.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateMoveBlockingRules(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
patterns []string
|
||||
logTag string
|
||||
assert func(t *testing.T, rules []string)
|
||||
}{
|
||||
{
|
||||
name: "single literal path",
|
||||
patterns: []string{"/sensitive/data"},
|
||||
logTag: "test",
|
||||
assert: func(t *testing.T, rules []string) {
|
||||
// Should block moving the path itself
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (subpath \"/sensitive/data\") (with message \"test\"))")
|
||||
// Should block moving the parent directory
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (literal \"/sensitive\") (with message \"test\"))")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "glob pattern",
|
||||
patterns: []string{"/path/to/*.txt"},
|
||||
logTag: "test",
|
||||
assert: func(t *testing.T, rules []string) {
|
||||
// Should block moving the base directory
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (subpath \"/path/to\") (with message \"test\"))")
|
||||
// Should block moving ancestor directories
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (literal \"/path\") (with message \"test\"))")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple paths",
|
||||
patterns: []string{"/tmp/test", "/var/log/app"},
|
||||
logTag: "test",
|
||||
assert: func(t *testing.T, rules []string) {
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (subpath \"/tmp/test\") (with message \"test\"))")
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (literal \"/tmp\") (with message \"test\"))")
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (subpath \"/var/log/app\") (with message \"test\"))")
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (literal \"/var/log\") (with message \"test\"))")
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (literal \"/var\") (with message \"test\"))")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deep nested path",
|
||||
patterns: []string{"/a/b/c/d/e/file.txt"},
|
||||
logTag: "test",
|
||||
assert: func(t *testing.T, rules []string) {
|
||||
// Should have rules for all ancestors
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (literal \"/a/b/c/d/e\") (with message \"test\"))")
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (literal \"/a/b/c/d\") (with message \"test\"))")
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (literal \"/a/b/c\") (with message \"test\"))")
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (literal \"/a/b\") (with message \"test\"))")
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (literal \"/a\") (with message \"test\"))")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "root level path",
|
||||
patterns: []string{"/file"},
|
||||
logTag: "test",
|
||||
assert: func(t *testing.T, rules []string) {
|
||||
// Should only have the file itself, no ancestors
|
||||
assert.Contains(t, rules, "(deny file-write-unlink (subpath \"/file\") (with message \"test\"))")
|
||||
// Should not contain root as ancestor
|
||||
for _, rule := range rules {
|
||||
assert.NotContains(t, rule, "(deny file-write-unlink (literal \"/\"))")
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rules := generateMoveBlockingRules(tt.patterns, tt.logTag)
|
||||
tt.assert(t, rules)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemTranslationWithMoveProtection(t *testing.T) {
|
||||
policy := &sandbox.SandboxPolicy{
|
||||
Name: "test",
|
||||
Description: "test with move protection",
|
||||
PackageManagers: []string{"npm"},
|
||||
Filesystem: sandbox.FilesystemPolicy{
|
||||
DenyRead: []string{"/private/sensitive"},
|
||||
DenyWrite: []string{"/usr/local/bin"},
|
||||
},
|
||||
}
|
||||
|
||||
translator := newSeatbeltPolicyTranslator()
|
||||
actual, err := translator.translate(policy)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Should contain deny read rule
|
||||
assert.Contains(t, actual, "(deny file-read* (subpath \"/private/sensitive\") (with message")
|
||||
|
||||
// Should contain move protection for deny read
|
||||
assert.Contains(t, actual, ";; Prevent bypassing read restrictions via file movement")
|
||||
assert.Contains(t, actual, "(deny file-write-unlink (subpath \"/private/sensitive\") (with message")
|
||||
assert.Contains(t, actual, "(deny file-write-unlink (literal \"/private\") (with message")
|
||||
|
||||
// Should contain deny write rule
|
||||
assert.Contains(t, actual, "(deny file-write* (subpath \"/usr/local/bin\") (with message")
|
||||
|
||||
// Should contain move protection for deny write
|
||||
assert.Contains(t, actual, ";; Prevent bypassing write restrictions via file movement")
|
||||
assert.Contains(t, actual, "(deny file-write-unlink (subpath \"/usr/local/bin\") (with message")
|
||||
assert.Contains(t, actual, "(deny file-write-unlink (literal \"/usr/local\") (with message")
|
||||
assert.Contains(t, actual, "(deny file-write-unlink (literal \"/usr\") (with message")
|
||||
}
|
||||
|
||||
func TestPTYSupport(t *testing.T) {
|
||||
t.Run("PTY disabled by default", func(t *testing.T) {
|
||||
policy := &sandbox.SandboxPolicy{
|
||||
Name: "test",
|
||||
Description: "test without PTY",
|
||||
PackageManagers: []string{"npm"},
|
||||
AllowPTY: false,
|
||||
}
|
||||
|
||||
translator := newSeatbeltPolicyTranslator()
|
||||
actual, err := translator.translate(policy)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Should NOT contain PTY rules
|
||||
assert.NotContains(t, actual, "(allow pseudo-tty)")
|
||||
assert.NotContains(t, actual, "/dev/ptmx")
|
||||
})
|
||||
|
||||
t.Run("PTY enabled when requested", func(t *testing.T) {
|
||||
policy := &sandbox.SandboxPolicy{
|
||||
Name: "test",
|
||||
Description: "test with PTY",
|
||||
PackageManagers: []string{"npm"},
|
||||
AllowPTY: true,
|
||||
}
|
||||
|
||||
translator := newSeatbeltPolicyTranslator()
|
||||
actual, err := translator.translate(policy)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Should contain PTY rules
|
||||
assert.Contains(t, actual, ";; Pseudo-terminal (PTY) support")
|
||||
assert.Contains(t, actual, "(allow pseudo-tty)")
|
||||
assert.Contains(t, actual, "(allow file-ioctl")
|
||||
assert.Contains(t, actual, "(literal \"/dev/ptmx\")")
|
||||
assert.Contains(t, actual, "(regex #\"^/dev/ttys\")")
|
||||
assert.Contains(t, actual, "(allow file-read* file-write*")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user