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:
+91
-11
@@ -3,23 +3,37 @@ package sandbox
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/safedep/dry/utils"
|
||||
)
|
||||
|
||||
// SandboxPolicy represents a parsed and validated sandbox policy that defines
|
||||
// filesystem, network, and process execution restrictions for package managers.
|
||||
// Policy violations will block execution.
|
||||
// Policy violations will block execution. Policy supports inheritance from parent policies.
|
||||
type SandboxPolicy struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
PackageManagers []string `yaml:"package_managers" json:"package_managers"`
|
||||
Filesystem FilesystemPolicy `yaml:"filesystem" json:"filesystem"`
|
||||
Network NetworkPolicy `yaml:"network" json:"network"`
|
||||
Process ProcessPolicy `yaml:"process" json:"process"`
|
||||
// These fields are not affected by inheritance and are set from the child policy.
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Inherits string `yaml:"inherits,omitempty" json:"inherits,omitempty"`
|
||||
PackageManagers []string `yaml:"package_managers" json:"package_managers"`
|
||||
|
||||
// These fields are affected by inheritance and are merged with the parent policy.
|
||||
// Any new values added here should be handled in the MergeWithParent method.
|
||||
Filesystem FilesystemPolicy `yaml:"filesystem" json:"filesystem"`
|
||||
Network NetworkPolicy `yaml:"network" json:"network"`
|
||||
Process ProcessPolicy `yaml:"process" json:"process"`
|
||||
|
||||
// The boolean fields are pointers to allow for nil values so that the YAML parser
|
||||
// can set the values from the child policy if present. We can differentiate between
|
||||
// nil and false values. Any new values added here should be handled in the
|
||||
// MergeWithParent method. When present in the child policy, the parent value is ignored.
|
||||
// When not present in the child policy, the parent value is used.
|
||||
|
||||
// AllowGitConfig allows write access to .git/config file.
|
||||
AllowGitConfig bool `yaml:"allow_git_config" json:"allow_git_config"`
|
||||
AllowGitConfig *bool `yaml:"allow_git_config" json:"allow_git_config"`
|
||||
|
||||
// AllowPTY allows pseudo-terminal (PTY) operations.
|
||||
AllowPTY bool `yaml:"allow_pty" json:"allow_pty"`
|
||||
AllowPTY *bool `yaml:"allow_pty" json:"allow_pty"`
|
||||
}
|
||||
|
||||
// FilesystemPolicy defines allowed and denied filesystem access patterns.
|
||||
@@ -45,8 +59,10 @@ type ProcessPolicy struct {
|
||||
DenyExec []string `yaml:"deny_exec" json:"deny_exec"`
|
||||
}
|
||||
|
||||
// Validate validates the sandbox policy for correctness.
|
||||
// Validate validates the sandbox policy for correctness before inheritance resolution.
|
||||
// Returns an error if the policy is invalid.
|
||||
// Note: Validation for "at least one rule" check is deferred to ValidateResolved(),
|
||||
// since a child policy might have no rules of its own but inherit rules from its parent.
|
||||
func (p *SandboxPolicy) Validate() error {
|
||||
if p.Name == "" {
|
||||
return fmt.Errorf("policy name is required")
|
||||
@@ -56,6 +72,17 @@ func (p *SandboxPolicy) Validate() error {
|
||||
return fmt.Errorf("policy must specify at least one package manager")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateResolved validates a policy after inheritance has been resolved.
|
||||
// This is called after MergeWithParent to ensure the final policy is valid.
|
||||
func (p *SandboxPolicy) ValidateResolved() error {
|
||||
if err := p.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check that at least one access rule is defined (after inheritance)
|
||||
hasRules := len(p.Filesystem.AllowRead) > 0 ||
|
||||
len(p.Filesystem.AllowWrite) > 0 ||
|
||||
len(p.Filesystem.DenyRead) > 0 ||
|
||||
@@ -66,7 +93,7 @@ func (p *SandboxPolicy) Validate() error {
|
||||
len(p.Process.DenyExec) > 0
|
||||
|
||||
if !hasRules {
|
||||
return fmt.Errorf("policy must define at least one access rule")
|
||||
return fmt.Errorf("policy must define at least one access rule (after inheritance resolution)")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -83,3 +110,56 @@ func (p *SandboxPolicy) AppliesToPackageManager(pm string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// MergeWithParent merges the child policy with its parent policy.
|
||||
// Lists are unioned (additive), package_managers are replaced, booleans are overridden.
|
||||
// This method modifies the receiver (child policy) in place.
|
||||
func (child *SandboxPolicy) MergeWithParent(parent *SandboxPolicy) {
|
||||
// Union filesystem lists
|
||||
child.Filesystem.AllowRead = unionStringSlices(parent.Filesystem.AllowRead, child.Filesystem.AllowRead)
|
||||
child.Filesystem.AllowWrite = unionStringSlices(parent.Filesystem.AllowWrite, child.Filesystem.AllowWrite)
|
||||
child.Filesystem.DenyRead = unionStringSlices(parent.Filesystem.DenyRead, child.Filesystem.DenyRead)
|
||||
child.Filesystem.DenyWrite = unionStringSlices(parent.Filesystem.DenyWrite, child.Filesystem.DenyWrite)
|
||||
|
||||
// Union network lists
|
||||
child.Network.AllowOutbound = unionStringSlices(parent.Network.AllowOutbound, child.Network.AllowOutbound)
|
||||
child.Network.DenyOutbound = unionStringSlices(parent.Network.DenyOutbound, child.Network.DenyOutbound)
|
||||
|
||||
// Union process lists
|
||||
child.Process.AllowExec = unionStringSlices(parent.Process.AllowExec, child.Process.AllowExec)
|
||||
child.Process.DenyExec = unionStringSlices(parent.Process.DenyExec, child.Process.DenyExec)
|
||||
|
||||
// Set boolean fields by duplicating the parent value if not present in the child.
|
||||
if child.AllowPTY == nil {
|
||||
child.AllowPTY = utils.PtrTo(utils.SafelyGetValue(parent.AllowPTY))
|
||||
}
|
||||
|
||||
if child.AllowGitConfig == nil {
|
||||
child.AllowGitConfig = utils.PtrTo(utils.SafelyGetValue(parent.AllowGitConfig))
|
||||
}
|
||||
}
|
||||
|
||||
// unionStringSlices returns a new slice containing all unique elements from both slices.
|
||||
// Order is preserved: parent entries first, then child entries (excluding duplicates).
|
||||
func unionStringSlices(parent, child []string) []string {
|
||||
seen := make(map[string]bool)
|
||||
result := make([]string, 0, len(parent)+len(child))
|
||||
|
||||
// Add all parent entries
|
||||
for _, item := range parent {
|
||||
if !seen[item] {
|
||||
seen[item] = true
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
// Add child entries that aren't duplicates
|
||||
for _, item := range child {
|
||||
if !seen[item] {
|
||||
seen[item] = true
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user