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") } }