- FIND-SEC-01: GetUserContext requires session ownership (handler 403 + SQL/memory owner filter); in-memory HTTP + isolated-PG regressions - FIND-ARCH-01: archtest fails closed on untracked table writes (+noise exemption) and registers 50 existing writer tables in table_owners - FIND-SEC-02: Authorizer without pool denies by default; explicit NewInMemoryAuthorizer seam opt-in via ALLOW_IN_MEMORY at composition - FIND-SEC-03: swboot commands run argv-exec (no sh -c) with exact basename allowlist match; prefix-spoof and no-shell-eval tests - FIND-FLOW-01: confirm rejects validated_only plans; ExecuteClaimed hard-refuses validated_only steps (new error codes + HTTP mapping) - FIND-QUAL-01: intentflow.ValidateFlagConsistency fail-fast at Assemble
204 lines
7.3 KiB
Go
204 lines
7.3 KiB
Go
package swboot
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type fakeAllowlist struct {
|
|
paths map[string]bool
|
|
commands map[string][]string
|
|
}
|
|
|
|
func (f *fakeAllowlist) IsAllowlisted(p string) bool { return f.paths[filepath.Clean(p)] }
|
|
func (f *fakeAllowlist) AllowedCommands(p string) []string {
|
|
return f.commands[filepath.Clean(p)]
|
|
}
|
|
|
|
func initTestRepo(t *testing.T) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
run := func(args ...string) {
|
|
c := exec.Command("git", args...)
|
|
c.Dir = dir
|
|
c.Env = []string{"PATH=" + os.Getenv("PATH"), "GIT_TERMINAL_PROMPT=0",
|
|
"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t",
|
|
"GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_SYSTEM=/dev/null"}
|
|
if out, err := c.CombinedOutput(); err != nil {
|
|
t.Fatalf("git %v: %v: %s", args, err, out)
|
|
}
|
|
}
|
|
run("init", "-q", "-b", "main")
|
|
if err := os.WriteFile(filepath.Join(dir, "hello.txt"), []byte("hello\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
run("add", ".")
|
|
run("commit", "-q", "-m", "init")
|
|
return dir
|
|
}
|
|
|
|
const samplePatch = `--- a/hello.txt
|
|
+++ b/hello.txt
|
|
@@ -1 +1 @@
|
|
-hello
|
|
+hello swboot
|
|
`
|
|
|
|
func newExecutor(allow RepoAllowlistReader) *ControlledPatchExecutor {
|
|
return &ControlledPatchExecutor{Allowlist: allow}
|
|
}
|
|
|
|
func TestExecutor_RejectsNonAllowlistedPath(t *testing.T) {
|
|
repo := initTestRepo(t)
|
|
e := newExecutor(&fakeAllowlist{paths: map[string]bool{"/definitely/not/allowed": true}})
|
|
res, err := e.Execute(context.Background(), ControlledPatchRequest{
|
|
ProjectID: 1, RepoPath: repo, BaseRef: "HEAD", Patches: []string{samplePatch},
|
|
})
|
|
if err == nil || res.Status != "failed" || !strings.Contains(res.Error, "ERR_REPO_PATH_NOT_ALLOWLISTED") {
|
|
t.Fatalf("expected allowlist rejection, got res=%+v err=%v", res, err)
|
|
}
|
|
if res.WorktreePath != "" {
|
|
t.Fatalf("no worktree must be created on rejection")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_AppliesPatchOnIsolatedWorktree(t *testing.T) {
|
|
repo := initTestRepo(t)
|
|
// Baseline untouched.
|
|
e := newExecutor(&fakeAllowlist{paths: map[string]bool{repo: true}})
|
|
res, err := e.Execute(context.Background(), ControlledPatchRequest{
|
|
ProjectID: 7, RepoPath: repo, BaseRef: "HEAD", Patches: []string{samplePatch},
|
|
})
|
|
if err != nil || res.Status != "succeeded" {
|
|
t.Fatalf("expected success, got res=%+v err=%v", res, err)
|
|
}
|
|
if res.PatchesApplied != 1 || len(res.Files) != 1 || res.Files[0].Path != "hello.txt" || res.Files[0].Sha == "" {
|
|
t.Fatalf("unexpected files evidence: %+v", res.Files)
|
|
}
|
|
if !strings.HasPrefix(res.Branch, "feature/swboot-") || res.WorktreePath == "" {
|
|
t.Fatalf("expected isolated branch/worktree: %+v", res)
|
|
}
|
|
// Patch landed only in the isolated worktree.
|
|
patched, err := os.ReadFile(filepath.Join(res.WorktreePath, "hello.txt"))
|
|
if err != nil || !strings.Contains(string(patched), "hello swboot") {
|
|
t.Fatalf("worktree not patched: %v %q", err, patched)
|
|
}
|
|
base, _ := os.ReadFile(filepath.Join(repo, "hello.txt"))
|
|
if strings.Contains(string(base), "swboot") {
|
|
t.Fatalf("repository working tree must stay untouched")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_RunsOnlyAllowlistedCommands(t *testing.T) {
|
|
repo := initTestRepo(t)
|
|
e := newExecutor(&fakeAllowlist{
|
|
paths: map[string]bool{repo: true},
|
|
commands: map[string][]string{repo: {"echo"}},
|
|
})
|
|
res, err := e.Execute(context.Background(), ControlledPatchRequest{
|
|
ProjectID: 8, RepoPath: repo, BaseRef: "HEAD",
|
|
Patches: []string{samplePatch},
|
|
Commands: []string{"echo ci-ok"},
|
|
})
|
|
if err != nil || res.Status != "succeeded" {
|
|
t.Fatalf("expected success: %+v %v", res, err)
|
|
}
|
|
if len(res.Commands) != 1 || res.Commands[0].ExitCode != 0 || !strings.Contains(res.Commands[0].Output, "ci-ok") {
|
|
t.Fatalf("command evidence wrong: %+v", res.Commands)
|
|
}
|
|
|
|
res2, err2 := e.Execute(context.Background(), ControlledPatchRequest{
|
|
ProjectID: 8, RepoPath: repo, BaseRef: "HEAD",
|
|
Patches: []string{samplePatch},
|
|
Commands: []string{"curl http://169.254.169.254/latest/meta-data"},
|
|
})
|
|
if err2 == nil || res2.Status != "failed" || !strings.Contains(res2.Error, "not in admin allowlist") {
|
|
t.Fatalf("non-allowlisted command must fail closed: %+v %v", res2, err2)
|
|
}
|
|
}
|
|
|
|
func TestExecutor_RejectsBrokenPatchAndKeepsEvidence(t *testing.T) {
|
|
repo := initTestRepo(t)
|
|
e := newExecutor(&fakeAllowlist{paths: map[string]bool{repo: true}})
|
|
res, err := e.Execute(context.Background(), ControlledPatchRequest{
|
|
ProjectID: 9, RepoPath: repo, BaseRef: "HEAD",
|
|
Patches: []string{"not a unified diff"},
|
|
})
|
|
if err == nil || res.Status != "failed" {
|
|
t.Fatalf("broken patch must fail: %+v %v", res, err)
|
|
}
|
|
if res.PatchesApplied != 0 {
|
|
t.Fatalf("no patch applied on failure: %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestExecutor_CommandFailureStopsPipeline(t *testing.T) {
|
|
repo := initTestRepo(t)
|
|
e := newExecutor(&fakeAllowlist{
|
|
paths: map[string]bool{repo: true},
|
|
commands: map[string][]string{repo: {"false", "echo"}},
|
|
})
|
|
res, err := e.Execute(context.Background(), ControlledPatchRequest{
|
|
ProjectID: 10, RepoPath: repo, BaseRef: "HEAD",
|
|
Patches: []string{samplePatch},
|
|
Commands: []string{"false", "echo unreachable"},
|
|
})
|
|
if err == nil || res.Status != "failed" {
|
|
t.Fatalf("failing command must stop: %+v %v", res, err)
|
|
}
|
|
if len(res.Commands) != 1 || res.Commands[0].ExitCode == 0 {
|
|
t.Fatalf("first command must record failure and stop: %+v", res.Commands)
|
|
}
|
|
}
|
|
|
|
func TestApproveMerge_NeverExecutes(t *testing.T) {
|
|
if err := ApproveMerge("anything"); err == nil || !strings.Contains(err.Error(), "ERR_MERGE_REQUIRES_SEPARATE_AUTHORIZATION") {
|
|
t.Fatalf("merge must stay behind separate authorization: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestExecutor_CommandAllowlistIsExactNotPrefix FIND-SEC-03 回归:
|
|
// 白名单是精确命令名匹配,前缀伪装(echo 放行 echo-evil)必须被拒。
|
|
func TestExecutor_CommandAllowlistIsExactNotPrefix(t *testing.T) {
|
|
if commandAllowed("echo-evil pwn", []string{"echo"}) {
|
|
t.Fatal("prefix spoof 'echo-evil' must not match allowlist entry 'echo'")
|
|
}
|
|
if !commandAllowed("echo ci-ok", []string{"echo"}) {
|
|
t.Fatal("exact command 'echo' must match")
|
|
}
|
|
if !commandAllowed("/usr/bin/echo hi", []string{"echo"}) {
|
|
t.Fatal("absolute-path basename must match entry 'echo'")
|
|
}
|
|
if commandAllowed("", []string{"echo"}) || commandAllowed(" ", []string{"echo"}) {
|
|
t.Fatal("empty command must be rejected")
|
|
}
|
|
}
|
|
|
|
// TestExecutor_NoShellEvaluation FIND-SEC-03 回归:argv 化执行后,命令行中\n// 的 shell 元字符只作为字面参数传给命令,不会被 shell 求值。
|
|
func TestExecutor_NoShellEvaluation(t *testing.T) {
|
|
repo := initTestRepo(t)
|
|
marker := filepath.Join(t.TempDir(), "sh-pwn-marker")
|
|
e := newExecutor(&fakeAllowlist{
|
|
paths: map[string]bool{repo: true},
|
|
commands: map[string][]string{repo: {"echo"}},
|
|
})
|
|
res, err := e.Execute(context.Background(), ControlledPatchRequest{
|
|
ProjectID: 11, RepoPath: repo, BaseRef: "HEAD",
|
|
Patches: []string{samplePatch},
|
|
Commands: []string{"echo x && touch " + marker},
|
|
})
|
|
if err != nil || res.Status != "succeeded" {
|
|
t.Fatalf("echo with literal metachar args should succeed: %+v %v", res, err)
|
|
}
|
|
if len(res.Commands) != 1 || !strings.Contains(res.Commands[0].Output, "x && touch") {
|
|
t.Fatalf("output should be literal echo args: %+v", res.Commands)
|
|
}
|
|
if _, statErr := os.Stat(marker); statErr == nil {
|
|
t.Fatal("shell metacharacters must not be evaluated; marker file was created")
|
|
}
|
|
}
|