Files
cc-connect/core/runas_check_test.go
T
2026-06-02 23:14:41 +08:00

229 lines
7.6 KiB
Go

//go:build !windows
package core
import (
"context"
"os/exec"
"strconv"
"strings"
"testing"
"time"
)
func TestPreflightRunAsUser_AllPass(t *testing.T) {
runner := &stubSudoRunner{
script: map[string]stubResponse{
key("-n", "-iu", "target", "--", "/usr/bin/true"): {nil, nil},
key("-n", "-iu", "target", "--", "sudo", "-n", "/usr/bin/true"): {nil, &exec.ExitError{}}, // escalation fails as required
key("-n", "-iu", "target", "--", "test", "-r", "/tmp/wd", "-a", "-w", "/tmp/wd"): {nil, nil},
},
}
// Build the find args the real scanner will use. For this test we
// just make the find call return empty output so no warnings are
// generated.
cfg := PreflightConfig{
Project: "demo",
RunAsUser: "target",
WorkDir: "/tmp/wd",
Runner: runner,
ScanConfig: DescendantScanConfig{
PrunePaths: []string{".git"},
MaxReport: 50,
Timeout: time.Second,
},
}
// Script the find call to return empty.
findArgs := []string{
"-n", "-iu", "target", "--",
"find", "/tmp/wd",
"(", "-name", ".git", ")", "-prune", "-o",
"(",
"-not", "-readable", "-printf", `noread\t%p\n`,
",",
"-type", "f", "-not", "-writable", "-printf", `nowrite\t%p\n`,
",",
"-type", "d", "-not", "-executable", "-printf", `nosearch\t%p\n`,
")",
}
runner.script[key(findArgs...)] = stubResponse{out: []byte(""), err: nil}
result := PreflightRunAsUser(context.Background(), cfg)
if result.HasFatal() {
t.Fatalf("want no fatal, got: %v", result.Fatal)
}
if len(result.Warnings) != 0 {
t.Fatalf("want no warnings, got: %v", result.Warnings)
}
}
func TestPreflightRunAsUser_NoSudoToTargetIsFatal(t *testing.T) {
runner := &stubSudoRunner{
script: map[string]stubResponse{
key("-n", "-iu", "target", "--", "/usr/bin/true"): {[]byte("a password is required"), &exec.ExitError{}},
},
}
cfg := PreflightConfig{Project: "demo", RunAsUser: "target", WorkDir: "/tmp/wd", Runner: runner}
result := PreflightRunAsUser(context.Background(), cfg)
if !result.HasFatal() {
t.Fatal("want fatal error")
}
if !strings.Contains(result.Fatal[0].Error(), "passwordless sudo to user") {
t.Errorf("want 'passwordless sudo to user' in error, got %v", result.Fatal[0])
}
}
func TestPreflightRunAsUser_TargetCanEscalateIsFatal(t *testing.T) {
runner := &stubSudoRunner{
script: map[string]stubResponse{
key("-n", "-iu", "target", "--", "/usr/bin/true"): {nil, nil},
key("-n", "-iu", "target", "--", "sudo", "-n", "/usr/bin/true"): {nil, nil}, // BAD
key("-n", "-iu", "target", "--", "sudo", "-n", "-l"): {[]byte("(ALL) NOPASSWD: ALL"), nil},
key("-n", "-iu", "target", "--", "test", "-r", "/tmp/wd", "-a", "-w", "/tmp/wd"): {nil, nil},
},
}
findArgs := []string{
"-n", "-iu", "target", "--",
"find", "/tmp/wd",
"(",
"-not", "-readable", "-printf", `noread\t%p\n`,
",",
"-type", "f", "-not", "-writable", "-printf", `nowrite\t%p\n`,
",",
"-type", "d", "-not", "-executable", "-printf", `nosearch\t%p\n`,
")",
}
runner.script[key(findArgs...)] = stubResponse{out: nil, err: nil}
cfg := PreflightConfig{
Project: "demo",
RunAsUser: "target",
WorkDir: "/tmp/wd",
Runner: runner,
ScanConfig: DescendantScanConfig{MaxReport: 50, Timeout: time.Second},
}
result := PreflightRunAsUser(context.Background(), cfg)
if !result.HasFatal() {
t.Fatal("want fatal error")
}
if !strings.Contains(result.Fatal[0].Error(), "can run passwordless sudo") {
t.Errorf("want 'can run passwordless sudo' in error, got %v", result.Fatal[0])
}
if !strings.Contains(result.Fatal[0].Error(), "NOPASSWD: ALL") {
t.Errorf("want sudo -l output included in error, got %v", result.Fatal[0])
}
}
func TestPreflightRunAsUser_WorkDirInaccessibleIsFatal(t *testing.T) {
runner := &stubSudoRunner{
script: map[string]stubResponse{
key("-n", "-iu", "target", "--", "/usr/bin/true"): {nil, nil},
key("-n", "-iu", "target", "--", "sudo", "-n", "/usr/bin/true"): {nil, &exec.ExitError{}},
key("-n", "-iu", "target", "--", "test", "-r", "/tmp/wd", "-a", "-w", "/tmp/wd"): {[]byte(""), &exec.ExitError{}},
},
}
cfg := PreflightConfig{
Project: "demo",
RunAsUser: "target",
WorkDir: "/tmp/wd",
Runner: runner,
ScanConfig: DescendantScanConfig{MaxReport: 50, Timeout: time.Second},
}
result := PreflightRunAsUser(context.Background(), cfg)
if !result.HasFatal() {
t.Fatal("want fatal error")
}
if !strings.Contains(result.Fatal[0].Error(), "cannot read AND write work_dir") {
t.Errorf("want 'cannot read AND write work_dir' in error, got %v", result.Fatal[0])
}
}
func TestPreflightRunAsUser_DescendantWarnings(t *testing.T) {
runner := &stubSudoRunner{
script: map[string]stubResponse{
key("-n", "-iu", "target", "--", "/usr/bin/true"): {nil, nil},
key("-n", "-iu", "target", "--", "sudo", "-n", "/usr/bin/true"): {nil, &exec.ExitError{}},
key("-n", "-iu", "target", "--", "test", "-r", "/tmp/wd", "-a", "-w", "/tmp/wd"): {nil, nil},
},
}
findArgs := []string{
"-n", "-iu", "target", "--",
"find", "/tmp/wd",
"(",
"-not", "-readable", "-printf", `noread\t%p\n`,
",",
"-type", "f", "-not", "-writable", "-printf", `nowrite\t%p\n`,
",",
"-type", "d", "-not", "-executable", "-printf", `nosearch\t%p\n`,
")",
}
runner.script[key(findArgs...)] = stubResponse{
out: []byte("noread\t/tmp/wd/secret.env\nnowrite\t/tmp/wd/readonly.log\n"),
err: nil,
}
cfg := PreflightConfig{
Project: "demo",
RunAsUser: "target",
WorkDir: "/tmp/wd",
Runner: runner,
ScanConfig: DescendantScanConfig{MaxReport: 50, Timeout: time.Second},
}
result := PreflightRunAsUser(context.Background(), cfg)
if result.HasFatal() {
t.Fatalf("want no fatal, got: %v", result.Fatal)
}
if len(result.Warnings) != 1 {
t.Fatalf("want 1 warning, got %d: %v", len(result.Warnings), result.Warnings)
}
if !strings.Contains(result.Warnings[0], "secret.env") || !strings.Contains(result.Warnings[0], "readonly.log") {
t.Errorf("warning missing expected paths: %s", result.Warnings[0])
}
}
func TestPreflightRunAsUser_DescendantWarningsCapped(t *testing.T) {
runner := &stubSudoRunner{
script: map[string]stubResponse{
key("-n", "-iu", "target", "--", "/usr/bin/true"): {nil, nil},
key("-n", "-iu", "target", "--", "sudo", "-n", "/usr/bin/true"): {nil, &exec.ExitError{}},
key("-n", "-iu", "target", "--", "test", "-r", "/tmp/wd", "-a", "-w", "/tmp/wd"): {nil, nil},
},
}
// Generate 75 lines; cap is 3.
var sb strings.Builder
for i := 0; i < 75; i++ {
sb.WriteString("noread\t/tmp/wd/file")
sb.WriteString(strconv.Itoa(i))
sb.WriteByte('\n')
}
findArgs := []string{
"-n", "-iu", "target", "--",
"find", "/tmp/wd",
"(",
"-not", "-readable", "-printf", `noread\t%p\n`,
",",
"-type", "f", "-not", "-writable", "-printf", `nowrite\t%p\n`,
",",
"-type", "d", "-not", "-executable", "-printf", `nosearch\t%p\n`,
")",
}
runner.script[key(findArgs...)] = stubResponse{out: []byte(sb.String()), err: nil}
cfg := PreflightConfig{
Project: "demo",
RunAsUser: "target",
WorkDir: "/tmp/wd",
Runner: runner,
ScanConfig: DescendantScanConfig{MaxReport: 3, Timeout: time.Second},
}
result := PreflightRunAsUser(context.Background(), cfg)
if result.HasFatal() {
t.Fatalf("want no fatal, got: %v", result.Fatal)
}
if len(result.Warnings) != 1 {
t.Fatalf("want 1 warning, got %d", len(result.Warnings))
}
w := result.Warnings[0]
if !strings.Contains(w, "... and 72 more") {
t.Errorf("want '... and 72 more' in warning, got: %s", w)
}
}