mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat/sandbox allow explicit dangerous pattern override (#239)
* feat(sandbox): allow opt-out of mandatory deny via explicit allow rules
Mandatory deny patterns (.env, .aws, .ssh, .gcloud, .kube, .gnupg,
.docker/config.json, .git/config) can now be opted out by listing the
exact literal post-expansion path in policy filesystem.allow_read /
allow_write, OR via --sandbox-allow read=... / write=... at runtime.
Both channels are treated at par.
Suppression is exact-match. Listing the CWD-absolute or HOME-absolute
form of a dangerous file additionally suppresses its **/<file> glob
sibling on the same direction so a single opt-out is sufficient.
Broad globs (${CWD}/**) and relative paths in user allow lists do not
suppress. The unnamed absolute form remains denied. .git/hooks is
unconditional and never suppressible (arbitrary code execution risk).
GetMandatoryDenyPatterns now returns split DenyRead / DenyWrite
slices and reports SuppressedRead / SuppressedWrite for audit. Both
translators emit per-direction deny rules and log.Warnf each
suppression. On Linux/bubblewrap, the tmpfs hide is restricted to the
intersection of DenyRead and DenyWrite; one-sided suppression falls
back to /dev/null (write) or the user's allow_read --ro-bind (read).
bwrap has no primitive that allows writes while denying reads, so
write-only opt-outs warn that the read-side mandatory deny is
unenforceable.
Updates docs/sandbox.md to document the opt-out, exact-match
semantics, and the Linux platform limitation. Updates pmg-e2e.yml to
create ./.env so the sandbox e2e test exercises the BLOCK case.
Closes #232
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: Code review fixes
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
b56a8e2a43
commit
d6755d3f44
@@ -9,6 +9,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/dry/utils"
|
||||
"github.com/safedep/pmg/sandbox"
|
||||
"github.com/safedep/pmg/sandbox/util"
|
||||
@@ -482,28 +483,60 @@ func (t *seatbeltPolicyTranslator) translateFilesystem(policy *sandbox.SandboxPo
|
||||
sb.WriteString("\n")
|
||||
|
||||
if t.enableDangerousFileBlocking {
|
||||
// Add mandatory deny patterns for security (credentials, git hooks, etc.)
|
||||
sb.WriteString(";; Mandatory security denies (credentials, git hooks, etc.)\n")
|
||||
mandatoryDenies := util.GetMandatoryDenyPatterns(utils.SafelyGetValue(policy.AllowGitConfig))
|
||||
for _, pattern := range mandatoryDenies {
|
||||
// Expand variables if needed
|
||||
expandedAllowRead, err := expandAll(policy.Filesystem.AllowRead)
|
||||
if err != nil {
|
||||
log.Warnf("sandbox: failed to expand allow_read for mandatory deny suppression, all mandatory denies preserved: %v", err)
|
||||
expandedAllowRead = nil
|
||||
}
|
||||
expandedAllowWrite, err := expandAll(policy.Filesystem.AllowWrite)
|
||||
if err != nil {
|
||||
log.Warnf("sandbox: failed to expand allow_write for mandatory deny suppression, all mandatory denies preserved: %v", err)
|
||||
expandedAllowWrite = nil
|
||||
}
|
||||
|
||||
mandatoryResult := util.GetMandatoryDenyPatterns(util.MandatoryDenyOptions{
|
||||
AllowGitConfig: utils.SafelyGetValue(policy.AllowGitConfig),
|
||||
AllowRead: expandedAllowRead,
|
||||
AllowWrite: expandedAllowWrite,
|
||||
})
|
||||
|
||||
for _, p := range mandatoryResult.SuppressedRead {
|
||||
log.Warnf("sandbox: mandatory deny %q suppressed for read by explicit allow rule in policy %q", p, policy.Name)
|
||||
}
|
||||
for _, p := range mandatoryResult.SuppressedWrite {
|
||||
log.Warnf("sandbox: mandatory deny %q suppressed for write by explicit allow rule in policy %q", p, policy.Name)
|
||||
}
|
||||
|
||||
for _, pattern := range mandatoryResult.DenyWrite {
|
||||
expanded, err := util.ExpandVariables(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to expand mandatory deny pattern %s: %w", pattern, err)
|
||||
}
|
||||
|
||||
// Use regex matching for glob patterns, subpath for literals
|
||||
if util.ContainsGlob(expanded) {
|
||||
regexPattern := util.GlobToRegex(expanded)
|
||||
sb.WriteString(fmt.Sprintf("(deny file-write* (regex #\"%s\") (with message \"%s\"))\n", regexPattern, t.logTag))
|
||||
sb.WriteString(fmt.Sprintf("(deny file-read* (regex #\"%s\") (with message \"%s\"))\n", regexPattern, t.logTag))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("(deny file-write* (subpath \"%s\") (with message \"%s\"))\n", expanded, t.logTag))
|
||||
sb.WriteString(fmt.Sprintf("(deny file-read* (subpath \"%s\") (with message \"%s\"))\n", expanded, t.logTag))
|
||||
}
|
||||
expandedDenyWrite = append(expandedDenyWrite, expanded)
|
||||
}
|
||||
|
||||
for _, pattern := range mandatoryResult.DenyRead {
|
||||
expanded, err := util.ExpandVariables(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to expand mandatory deny pattern %s: %w", pattern, err)
|
||||
}
|
||||
|
||||
if util.ContainsGlob(expanded) {
|
||||
regexPattern := util.GlobToRegex(expanded)
|
||||
sb.WriteString(fmt.Sprintf("(deny file-read* (regex #\"%s\") (with message \"%s\"))\n", regexPattern, t.logTag))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("(deny file-read* (subpath \"%s\") (with message \"%s\"))\n", expanded, t.logTag))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
@@ -616,3 +649,15 @@ func (t *seatbeltPolicyTranslator) translateProcess(policy *sandbox.SandboxPolic
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func expandAll(patterns []string) ([]string, error) {
|
||||
out := make([]string, 0, len(patterns))
|
||||
for _, p := range patterns {
|
||||
expanded, err := util.ExpandVariables(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to expand pattern %q: %w", p, err)
|
||||
}
|
||||
out = append(out, expanded)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user