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:
Abhisek Datta
2026-05-06 12:45:36 +05:30
committed by GitHub
co-authored by Claude Opus 4.7
parent b56a8e2a43
commit d6755d3f44
12 changed files with 813 additions and 153 deletions
@@ -6,11 +6,14 @@ package platform
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/safedep/dry/utils"
"github.com/safedep/pmg/sandbox"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSeatbeltTranslatorDarwinCommonTranslation(t *testing.T) {
@@ -657,3 +660,59 @@ func TestSeatbeltTranslatorDarwinLogTag(t *testing.T) {
translator = &seatbeltPolicyTranslator{logTag: "test"}
assert.Equal(t, "test", translator.LogTag())
}
func TestSeatbeltMandatoryDenySuppression(t *testing.T) {
cwd, err := os.Getwd()
require.NoError(t, err)
t.Run("allow_read with literal CWD .env removes file-read* deny but keeps file-write* deny", func(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Name: "test",
Filesystem: sandbox.FilesystemPolicy{
AllowRead: []string{filepath.Join(cwd, ".env")},
},
}
out := translateFilesystemForTest(t, policy)
assert.NotContains(t, out, fmt.Sprintf(`(deny file-read* (subpath "%s")`, filepath.Join(cwd, ".env")))
assert.Contains(t, out, fmt.Sprintf(`(deny file-write* (subpath "%s")`, filepath.Join(cwd, ".env")))
})
t.Run("allow_write with literal CWD .env removes file-write* deny but keeps file-read* deny", func(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Name: "test",
Filesystem: sandbox.FilesystemPolicy{
AllowWrite: []string{filepath.Join(cwd, ".env")},
},
}
out := translateFilesystemForTest(t, policy)
assert.NotContains(t, out, fmt.Sprintf(`(deny file-write* (subpath "%s")`, filepath.Join(cwd, ".env")))
assert.Contains(t, out, fmt.Sprintf(`(deny file-read* (subpath "%s")`, filepath.Join(cwd, ".env")))
})
t.Run("broad glob does NOT remove mandatory denies", func(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Name: "test",
Filesystem: sandbox.FilesystemPolicy{
AllowRead: []string{filepath.Join(cwd, "**")},
},
}
out := translateFilesystemForTest(t, policy)
home, err := os.UserHomeDir()
require.NoError(t, err)
assert.Contains(t, out, fmt.Sprintf(`(deny file-read* (subpath "%s")`, filepath.Join(cwd, ".env")))
assert.Contains(t, out, fmt.Sprintf(`(deny file-read* (subpath "%s")`, filepath.Join(home, ".env")))
assert.Contains(t, out, fmt.Sprintf(`(deny file-read* (subpath "%s")`, filepath.Join(cwd, ".ssh")))
})
}
func translateFilesystemForTest(t *testing.T, policy *sandbox.SandboxPolicy) string {
t.Helper()
tr := newSeatbeltPolicyTranslator()
var sb strings.Builder
require.NoError(t, tr.translateFilesystem(policy, &sb))
return sb.String()
}