mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: Add support for policy inheritence (#113)
* 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>
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user