Files
pmg/sandbox/resolve.go
T
22d6eabb6b feat(sandbox): add network_via_proxy_only and allow_direct_dns policy fields (#370)
* feat(sandbox): add network_via_proxy_only and allow_direct_dns policy fields

Config surface for network lockdown: network_via_proxy_only confines a
sandboxed package manager's outbound network to the PMG proxy;
allow_direct_dns is its escape hatch re-opening direct DNS. Both follow
the existing pointer-bool inheritance pattern in MergeWithParent. Lint
warns when allow_direct_dns is set without network_via_proxy_only, where
it has no effect.

The fields are declared and inherited but unread; enforcement lands with
the ExecutionContext plumbing and Seatbelt lockdown translation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS

* fix(sandbox): deep-copy new pointer flags in profile resolution

expandPolicyPaths re-points the older pointer booleans so callers cannot
corrupt the registry-cached policy; the new NetworkViaProxyOnly and
AllowDirectDNS fields need the same isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-10 19:28:39 +05:30

118 lines
3.3 KiB
Go

package sandbox
import (
"fmt"
"github.com/safedep/dry/utils"
"github.com/safedep/pmg/sandbox/util"
)
// ResolveProfile loads name via the registry, resolves inheritance, and
// returns a SandboxPolicy with all path-bearing fields expanded against opts
// (or process env where opts fields are empty). The returned policy is a deep
// copy — the registry-cached policy is never mutated.
func (r *defaultProfileRegistry) ResolveProfile(name string, opts ResolveOptions) (*SandboxPolicy, error) {
policy, err := r.GetProfile(name)
if err != nil {
return nil, err
}
resolved, err := expandPolicyPaths(policy, opts)
if err != nil {
return nil, fmt.Errorf("failed to expand variables for profile %s: %w", name, err)
}
return resolved, nil
}
func expandPolicyPaths(p *SandboxPolicy, opts ResolveOptions) (*SandboxPolicy, error) {
// Shallow struct copy aliases *bool pointers with the registry-cached
// policy. Re-point them so callers mutating the result can't corrupt the
// cache. Slice fields below are rebuilt fresh so they're already isolated.
out := *p
if p.AllowGitConfig != nil {
out.AllowGitConfig = utils.PtrTo(*p.AllowGitConfig)
}
if p.AllowPTY != nil {
out.AllowPTY = utils.PtrTo(*p.AllowPTY)
}
if p.AllowNetworkBind != nil {
out.AllowNetworkBind = utils.PtrTo(*p.AllowNetworkBind)
}
if p.NetworkViaProxyOnly != nil {
out.NetworkViaProxyOnly = utils.PtrTo(*p.NetworkViaProxyOnly)
}
if p.AllowDirectDNS != nil {
out.AllowDirectDNS = utils.PtrTo(*p.AllowDirectDNS)
}
allowRead, err := expandSlice(p.Filesystem.AllowRead, opts)
if err != nil {
return nil, err
}
allowWrite, err := expandSlice(p.Filesystem.AllowWrite, opts)
if err != nil {
return nil, err
}
denyRead, err := expandSlice(p.Filesystem.DenyRead, opts)
if err != nil {
return nil, err
}
denyWrite, err := expandSlice(p.Filesystem.DenyWrite, opts)
if err != nil {
return nil, err
}
out.Filesystem = FilesystemPolicy{
AllowRead: allowRead,
AllowWrite: allowWrite,
DenyRead: denyRead,
DenyWrite: denyWrite,
}
allowExec, err := expandSlice(p.Process.AllowExec, opts)
if err != nil {
return nil, err
}
denyExec, err := expandSlice(p.Process.DenyExec, opts)
if err != nil {
return nil, err
}
out.Process = ProcessPolicy{AllowExec: allowExec, DenyExec: denyExec}
// Network entries are host:port and don't carry path variables, but the
// slices are still deep-copied so the caller can safely mutate the result.
out.Network = NetworkPolicy{
AllowOutbound: append([]string(nil), p.Network.AllowOutbound...),
DenyOutbound: append([]string(nil), p.Network.DenyOutbound...),
AllowBind: append([]string(nil), p.Network.AllowBind...),
}
// Environment entries are variable-name globs, not paths, so they are
// deep-copied without expansion so the caller can safely mutate the result.
out.Environment = EnvironmentPolicy{
Allow: append([]string(nil), p.Environment.Allow...),
Deny: append([]string(nil), p.Environment.Deny...),
}
out.PackageManagers = append([]string(nil), p.PackageManagers...)
return &out, nil
}
func expandSlice(in []string, opts ResolveOptions) ([]string, error) {
if in == nil {
return nil, nil
}
out := make([]string, len(in))
for i, p := range in {
exp, err := util.ExpandVariablesWith(p, opts.CWD, opts.Home, opts.TmpDir)
if err != nil {
return nil, err
}
out[i] = exp
}
return out, nil
}