|
| 1 | +package manifest |
| 2 | + |
| 3 | +import "testing" |
| 4 | + |
| 5 | +// proc.exec is a known capability and accepts an absolute-path target. |
| 6 | +func TestValidate_ProcExecAbsolutePath(t *testing.T) { |
| 7 | + t.Parallel() |
| 8 | + m := mustValid(t) |
| 9 | + m.Grants = append(m.Grants, Grant{Cap: "proc.exec", Target: "/usr/local/bin/weathercli"}) |
| 10 | + if errs := m.Validate(); len(errs) != 0 { |
| 11 | + t.Fatalf("proc.exec with an absolute path must validate, got: %v", errs) |
| 12 | + } |
| 13 | +} |
| 14 | + |
| 15 | +// proc.exec accepts a bare command name (resolved via PATH). |
| 16 | +func TestValidate_ProcExecBareCommand(t *testing.T) { |
| 17 | + t.Parallel() |
| 18 | + for _, cmd := range []string{"gh", "python3", "my-tool", "ripgrep"} { |
| 19 | + m := mustValid(t) |
| 20 | + m.Grants = append(m.Grants, Grant{Cap: "proc.exec", Target: cmd}) |
| 21 | + if errs := m.Validate(); len(errs) != 0 { |
| 22 | + t.Errorf("proc.exec %q must validate, got: %v", cmd, errs) |
| 23 | + } |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +// A proc.exec target must name exactly one binary: no wildcard, no shell, no |
| 28 | +// spaces, no path traversal. Each of these must be rejected. |
| 29 | +func TestValidate_ProcExecRejectsUnsafeTargets(t *testing.T) { |
| 30 | + t.Parallel() |
| 31 | + bad := []string{ |
| 32 | + "*", // wildcard — "run anything" is never allowed |
| 33 | + "/usr/bin/*", // path wildcard |
| 34 | + "sh -c 'rm -rf /'", // shell string with spaces |
| 35 | + "foo;bar", // command separator |
| 36 | + "foo|bar", // pipe |
| 37 | + "foo`id`", // command substitution |
| 38 | + "foo$(id)", // command substitution |
| 39 | + "../../bin/evil", // path traversal |
| 40 | + "/opt/../etc/cron.d/x", // traversal inside an absolute path |
| 41 | + "tool\nsecond", // newline injection |
| 42 | + } |
| 43 | + for _, target := range bad { |
| 44 | + m := mustValid(t) |
| 45 | + m.Grants = append(m.Grants, Grant{Cap: "proc.exec", Target: target}) |
| 46 | + if !hasErrorContaining(m.Validate(), "proc.exec") { |
| 47 | + t.Errorf("proc.exec target %q must be rejected, but validation passed", target) |
| 48 | + } |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +// An empty proc.exec target hits the generic empty-target error (not the |
| 53 | +// proc.exec-specific one), same as any other cap. |
| 54 | +func TestValidate_ProcExecEmptyTarget(t *testing.T) { |
| 55 | + t.Parallel() |
| 56 | + m := mustValid(t) |
| 57 | + m.Grants = append(m.Grants, Grant{Cap: "proc.exec", Target: " "}) |
| 58 | + if !hasErrorContaining(m.Validate(), "target must not be empty") { |
| 59 | + t.Errorf("empty proc.exec target should hit the empty-target error, got: %v", m.Validate()) |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +// Security boundary: proc.exec is NOT in the sideload allow-list, so a CLI app |
| 64 | +// (which carries a proc.exec grant) can never be sideloaded — it must go through |
| 65 | +// the reviewed catalogue. This pins that boundary against accidental widening. |
| 66 | +func TestEnforceSideloadPolicy_RejectsProcExec(t *testing.T) { |
| 67 | + t.Parallel() |
| 68 | + if _, ok := SideloadAllowedCaps["proc.exec"]; ok { |
| 69 | + t.Fatal("proc.exec must NOT be in the sideload allow-list (it is catalogue-only)") |
| 70 | + } |
| 71 | + m := baseSideloadOK() |
| 72 | + m.Grants = append(m.Grants, Grant{Cap: "proc.exec", Target: "/usr/local/bin/tool"}) |
| 73 | + if err := EnforceSideloadPolicy(m); err == nil { |
| 74 | + t.Fatal("sideload policy must reject a proc.exec grant") |
| 75 | + } |
| 76 | +} |
0 commit comments