Skip to content

Commit 3ef8bd9

Browse files
committed
feat: implement command family filtering in policy engine
1 parent 1f6af6f commit 3ef8bd9

3 files changed

Lines changed: 79 additions & 3 deletions

File tree

ROADMAP.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ This roadmap tracks the progress of the Sandforge Agent Sandbox based on [ARCHIT
77

88
- [x] **1.1 Project Scaffolding**: Go workspace, directory structure, and `go.mod`.
99
- [x] **1.2 Core API Contracts**: Define `SandboxSpec`, `ExecRequest`, and `SandboxBackend` interfaces.
10-
- [ ] **1.3 Policy Engine**:
10+
- [x] **1.3 Policy Engine**:
1111
- [x] Filesystem path validation (whitelist logic) [#1](https://github.com/yanurag-dev/sandforge/issues/1).
1212
- [x] Network mode enforcement (Offline/Fetch/Full) [#2](https://github.com/yanurag-dev/sandforge/issues/2).
1313
- [x] Resource limit validation (CPU/Memory/Disk) [#2](https://github.com/yanurag-dev/sandforge/issues/2).
14-
- [ ] Command family filtering [#3](https://github.com/yanurag-dev/sandforge/issues/3).
15-
- [ ] **1.4 Testing**: Unit tests for policy enforcement.
14+
- [x] Command family filtering [#3](https://github.com/yanurag-dev/sandforge/issues/3).
15+
- [x] **1.4 Testing**: Unit tests for policy enforcement.
1616

1717
## Phase 2: Orchestration & Mocking
1818
*Goal: Build the state machine that manages sandbox lifecycles.*

internal/policy/engine.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var (
1313
ErrPathNotAbs = errors.New("host path must be an absolute path")
1414
ErrResourceLimitExceeded = errors.New("requested resource exceeds policy limits")
1515
ErrInvalidNetworkMode = errors.New("requested network mode is not allowed")
16+
ErrForbiddenCommand = errors.New("command is not allowed by policy")
1617
)
1718

1819
type Engine struct {
@@ -22,6 +23,7 @@ type Engine struct {
2223
MaxMemoryMb int
2324
MaxDiskGb int
2425
AllowedNetworkModes []string
26+
AllowedCommands []string
2527
}
2628

2729
func (e *Engine) EvaluateMount(mount api.WorkspaceMount) error {
@@ -90,3 +92,23 @@ func (e *Engine) EvaluateSandbox(spec api.SandboxSpec) error {
9092
}
9193
return nil
9294
}
95+
96+
func (e *Engine) EvaluateExec (req api.ExecRequest) error {
97+
if len(req.Command) == 0 {
98+
return errors.New("no command provided")
99+
}
100+
101+
binary := req.Command[0]
102+
allowed := false
103+
for _, command := range e.AllowedCommands {
104+
if binary == command {
105+
allowed = true
106+
break
107+
}
108+
}
109+
110+
if !allowed {
111+
return ErrForbiddenCommand
112+
}
113+
return nil
114+
}

internal/policy/engine_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package policy
22

33
import (
4+
"errors"
45
"os"
56
"path/filepath"
67
"testing"
@@ -184,3 +185,56 @@ func TestEvaluateSandbox(t *testing.T) {
184185
})
185186
}
186187
}
188+
189+
func TestEvaluateExec(t *testing.T) {
190+
engine := &Engine{
191+
AllowedCommands: []string{"git", "npm", "ls"},
192+
}
193+
194+
tests := []struct {
195+
name string
196+
command []string
197+
wantError error
198+
}{
199+
{
200+
name: "Allowed command (git)",
201+
command: []string{"git", "push"},
202+
wantError: nil,
203+
},
204+
{
205+
name: "Allowed command (npm)",
206+
command: []string{"npm", "test"},
207+
wantError: nil,
208+
},
209+
{
210+
name: "Forbidden command (sudo)",
211+
command: []string{"sudo", "rm", "-rf", "/"},
212+
wantError: ErrForbiddenCommand,
213+
},
214+
{
215+
name: "Empty command slice",
216+
command: []string{},
217+
wantError: errors.New("no command provided"),
218+
},
219+
}
220+
221+
for _, tt := range tests {
222+
t.Run(tt.name, func(t *testing.T) {
223+
req := api.ExecRequest{
224+
Command: tt.command,
225+
}
226+
err := engine.EvaluateExec(req)
227+
228+
if tt.name == "Empty command slice" {
229+
if err == nil || err.Error() != tt.wantError.Error() {
230+
t.Errorf("EvaluateExec() error = %v, wantError %v", err, tt.wantError)
231+
}
232+
return
233+
}
234+
235+
if err != tt.wantError {
236+
t.Errorf("EvaluateExec() error = %v, wantError %v", err, tt.wantError)
237+
}
238+
})
239+
}
240+
}

0 commit comments

Comments
 (0)