mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* feat: Add support for policy inheritence * fix: Linter fixes * Update docs/sandbox.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com> * fix: Handle boolean inheritence * ci: Add linter * Update sandbox/policy_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com> * fix: Linter fixes * fix: Linter fixes * fix: Sandbox rule regex format --------- Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
289 lines
7.3 KiB
Go
289 lines
7.3 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/safedep/dry/utils"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestUnionStringSlices(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
parent []string
|
|
child []string
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "empty slices",
|
|
parent: []string{},
|
|
child: []string{},
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "parent empty, child has values",
|
|
parent: []string{},
|
|
child: []string{"a", "b"},
|
|
expected: []string{"a", "b"},
|
|
},
|
|
{
|
|
name: "parent has values, child empty",
|
|
parent: []string{"a", "b"},
|
|
child: []string{},
|
|
expected: []string{"a", "b"},
|
|
},
|
|
{
|
|
name: "no overlapping entries",
|
|
parent: []string{"a", "b"},
|
|
child: []string{"c", "d"},
|
|
expected: []string{"a", "b", "c", "d"},
|
|
},
|
|
{
|
|
name: "overlapping entries (deduplication)",
|
|
parent: []string{"a", "b", "c"},
|
|
child: []string{"b", "c", "d"},
|
|
expected: []string{"a", "b", "c", "d"},
|
|
},
|
|
{
|
|
name: "all duplicate entries",
|
|
parent: []string{"a", "b"},
|
|
child: []string{"a", "b"},
|
|
expected: []string{"a", "b"},
|
|
},
|
|
{
|
|
name: "parent has duplicates",
|
|
parent: []string{"a", "a", "b"},
|
|
child: []string{"c"},
|
|
expected: []string{"a", "b", "c"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := unionStringSlices(tt.parent, tt.child)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMergeWithParent(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
parent *SandboxPolicy
|
|
child *SandboxPolicy
|
|
assert func(t *testing.T, parent, child *SandboxPolicy)
|
|
}{
|
|
{
|
|
name: "merge when all fields are present",
|
|
parent: &SandboxPolicy{
|
|
Name: "parent",
|
|
Description: "Parent policy",
|
|
PackageManagers: []string{"npm", "yarn"},
|
|
AllowPTY: utils.PtrTo(true),
|
|
AllowGitConfig: utils.PtrTo(false),
|
|
Filesystem: FilesystemPolicy{
|
|
AllowRead: []string{"/usr/**", "/home/**"},
|
|
AllowWrite: []string{"/tmp/**"},
|
|
DenyRead: []string{"/etc/shadow"},
|
|
DenyWrite: []string{"/etc/**"},
|
|
},
|
|
Network: NetworkPolicy{
|
|
AllowOutbound: []string{"registry.npmjs.org:443"},
|
|
DenyOutbound: []string{"*:*"},
|
|
},
|
|
Process: ProcessPolicy{
|
|
AllowExec: []string{"/usr/bin/node"},
|
|
DenyExec: []string{"/usr/bin/curl"},
|
|
},
|
|
},
|
|
child: &SandboxPolicy{
|
|
Name: "child",
|
|
Description: "Child policy",
|
|
PackageManagers: []string{"npx"},
|
|
AllowPTY: utils.PtrTo(false),
|
|
AllowGitConfig: utils.PtrTo(true),
|
|
Filesystem: FilesystemPolicy{
|
|
AllowRead: []string{"/var/**"},
|
|
AllowWrite: []string{"/home/**"},
|
|
DenyRead: []string{},
|
|
DenyWrite: []string{"/usr/**"},
|
|
},
|
|
Network: NetworkPolicy{
|
|
AllowOutbound: []string{"github.com:443"},
|
|
DenyOutbound: []string{},
|
|
},
|
|
Process: ProcessPolicy{
|
|
AllowExec: []string{"/usr/bin/git"},
|
|
DenyExec: []string{},
|
|
},
|
|
},
|
|
assert: func(t *testing.T, parent, child *SandboxPolicy) {
|
|
// Test that name and description are preserved from child
|
|
assert.Equal(t, "child", child.Name)
|
|
assert.Equal(t, "Child policy", child.Description)
|
|
|
|
// Test that package managers are replaced (not merged)
|
|
assert.Equal(t, []string{"npx"}, child.PackageManagers)
|
|
|
|
// Test that boolean flags are overridden
|
|
assert.False(t, *child.AllowPTY)
|
|
assert.True(t, *child.AllowGitConfig)
|
|
|
|
// Test filesystem lists are unioned
|
|
assert.ElementsMatch(t, []string{"/usr/**", "/home/**", "/var/**"}, child.Filesystem.AllowRead)
|
|
assert.ElementsMatch(t, []string{"/tmp/**", "/home/**"}, child.Filesystem.AllowWrite)
|
|
assert.ElementsMatch(t, []string{"/etc/shadow"}, child.Filesystem.DenyRead)
|
|
assert.ElementsMatch(t, []string{"/etc/**", "/usr/**"}, child.Filesystem.DenyWrite)
|
|
|
|
// Test network lists are unioned
|
|
assert.ElementsMatch(t, []string{"registry.npmjs.org:443", "github.com:443"}, child.Network.AllowOutbound)
|
|
assert.ElementsMatch(t, []string{"*:*"}, child.Network.DenyOutbound)
|
|
|
|
// Test process lists are unioned
|
|
assert.ElementsMatch(t, []string{"/usr/bin/node", "/usr/bin/git"}, child.Process.AllowExec)
|
|
assert.ElementsMatch(t, []string{"/usr/bin/curl"}, child.Process.DenyExec)
|
|
},
|
|
},
|
|
{
|
|
name: "merge when child has no boolean fields",
|
|
parent: &SandboxPolicy{
|
|
Name: "parent",
|
|
Description: "Parent policy",
|
|
PackageManagers: []string{"npm", "yarn"},
|
|
AllowPTY: utils.PtrTo(true),
|
|
AllowGitConfig: utils.PtrTo(false),
|
|
},
|
|
child: &SandboxPolicy{
|
|
Filesystem: FilesystemPolicy{
|
|
AllowRead: []string{"/usr/**"},
|
|
},
|
|
},
|
|
assert: func(t *testing.T, parent, child *SandboxPolicy) {
|
|
// Child inherits boolean fields from parent if not present in child
|
|
assert.True(t, *child.AllowPTY)
|
|
assert.False(t, *child.AllowGitConfig)
|
|
|
|
// Still invalid because child has no name and package managers
|
|
assert.Error(t, child.Validate())
|
|
assert.Error(t, child.ValidateResolved())
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.child.MergeWithParent(tt.parent)
|
|
tt.assert(t, tt.parent, tt.child)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
policy *SandboxPolicy
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "valid policy without inheritance",
|
|
policy: &SandboxPolicy{
|
|
Name: "test",
|
|
PackageManagers: []string{"npm"},
|
|
Filesystem: FilesystemPolicy{
|
|
AllowRead: []string{"/usr/**"},
|
|
},
|
|
},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "missing name",
|
|
policy: &SandboxPolicy{
|
|
PackageManagers: []string{"npm"},
|
|
Filesystem: FilesystemPolicy{
|
|
AllowRead: []string{"/usr/**"},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "missing package managers",
|
|
policy: &SandboxPolicy{
|
|
Name: "test",
|
|
Filesystem: FilesystemPolicy{
|
|
AllowRead: []string{"/usr/**"},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "policy with inherits but no rules is valid",
|
|
policy: &SandboxPolicy{
|
|
Name: "test",
|
|
Inherits: "parent",
|
|
PackageManagers: []string{"npm"},
|
|
},
|
|
expectErr: false, // Basic validation allows this
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.policy.Validate()
|
|
if tt.expectErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateResolved(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
policy *SandboxPolicy
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "valid resolved policy",
|
|
policy: &SandboxPolicy{
|
|
Name: "test",
|
|
PackageManagers: []string{"npm"},
|
|
Filesystem: FilesystemPolicy{
|
|
AllowRead: []string{"/usr/**"},
|
|
},
|
|
},
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "resolved policy with no rules",
|
|
policy: &SandboxPolicy{
|
|
Name: "test",
|
|
PackageManagers: []string{"npm"},
|
|
},
|
|
expectErr: true, // Should fail because no rules after resolution
|
|
},
|
|
{
|
|
name: "missing name in resolved policy",
|
|
policy: &SandboxPolicy{
|
|
PackageManagers: []string{"npm"},
|
|
Filesystem: FilesystemPolicy{
|
|
AllowRead: []string{"/usr/**"},
|
|
},
|
|
},
|
|
expectErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.policy.ValidateResolved()
|
|
if tt.expectErr {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|