diff --git a/cmd/pkg-helper/main.go b/cmd/pkg-helper/main.go index b9962c22..22ec1db8 100644 --- a/cmd/pkg-helper/main.go +++ b/cmd/pkg-helper/main.go @@ -222,14 +222,25 @@ func handleRequest(req request) response { } } +var runApkFunc = runApk + +func runApk(args ...string) ([]byte, error) { + if os.Getuid() != 0 { + if sudo, err := exec.LookPath("sudo"); err == nil { + sudoArgs := append([]string{"apk"}, args...) + return exec.Command(sudo, sudoArgs...).CombinedOutput() + } + } + return exec.Command("apk", args...).CombinedOutput() +} + func doInstall(pkg string) response { apkMutex.Lock() defer apkMutex.Unlock() slog.Info("pkg-helper: installing", "package", pkg) - cmd := exec.Command("apk", "add", "--no-cache", pkg) - out, err := cmd.CombinedOutput() + out, err := runApkFunc("add", "--no-cache", pkg) if err != nil { msg, code := classifyApkOutput(string(out), err) slog.Error("pkg-helper: install failed", "package", pkg, "error", msg, "code", code) @@ -247,8 +258,7 @@ func doUninstall(pkg string) response { slog.Info("pkg-helper: uninstalling", "package", pkg) - cmd := exec.Command("apk", "del", pkg) - out, err := cmd.CombinedOutput() + out, err := runApkFunc("del", pkg) if err != nil { msg, code := classifyApkOutput(string(out), err) slog.Error("pkg-helper: uninstall failed", "package", pkg, "error", msg, "code", code) @@ -269,8 +279,7 @@ func doUpgrade(pkg string) response { slog.Info("pkg-helper: upgrading", "package", pkg) - cmd := exec.Command("apk", "add", "-u", pkg) - out, err := cmd.CombinedOutput() + out, err := runApkFunc("add", "-u", pkg) if err != nil { msg, code := classifyApkOutput(string(out), err) slog.Error("pkg-helper: upgrade failed", "package", pkg, "error", msg, "code", code) @@ -288,8 +297,7 @@ func doUpdateIndex() response { slog.Info("pkg-helper: updating index") - cmd := exec.Command("apk", "update") - out, err := cmd.CombinedOutput() + out, err := runApkFunc("update") if err != nil { msg, code := classifyApkOutput(string(out), err) slog.Warn("pkg-helper: update-index failed", "error", msg, "code", code) @@ -306,8 +314,7 @@ func doListOutdated() response { apkMutex.Lock() defer apkMutex.Unlock() - cmd := exec.Command("apk", "version", "-l", "<") - out, err := cmd.CombinedOutput() + out, err := runApkFunc("version", "-l", "<") if err != nil { msg, code := classifyApkOutput(string(out), err) return response{Error: msg, Code: code} diff --git a/cmd/pkg-helper/main_test.go b/cmd/pkg-helper/main_test.go index b4fc1b7a..80434bf6 100644 --- a/cmd/pkg-helper/main_test.go +++ b/cmd/pkg-helper/main_test.go @@ -12,6 +12,13 @@ import ( // Note: Command execution tests are not included here since apk is not available // in unit test environments. Integration tests would handle actual execution. func TestHandleRequest(t *testing.T) { + // Mock runApkFunc to avoid running actual commands or triggering sudo + origRunApkFunc := runApkFunc + runApkFunc = func(args ...string) ([]byte, error) { + return []byte("mock output"), nil + } + defer func() { runApkFunc = origRunApkFunc }() + tests := []struct { name string req request @@ -154,6 +161,13 @@ func TestValidPkgName(t *testing.T) { // TestHandleRequest_AllActionsValidated tests both install and uninstall actions. func TestHandleRequest_AllActionsValidated(t *testing.T) { + // Mock runApkFunc to avoid running actual commands or triggering sudo + origRunApkFunc := runApkFunc + runApkFunc = func(args ...string) ([]byte, error) { + return []byte("mock output"), nil + } + defer func() { runApkFunc = origRunApkFunc }() + tests := []struct { action string }{ @@ -365,6 +379,12 @@ func TestValidPkgNameRegex_Compliance(t *testing.T) { // TestHandleRequest_ErrorMessages tests that error messages are clear. func TestHandleRequest_ErrorMessages(t *testing.T) { + origRunApkFunc := runApkFunc + runApkFunc = func(args ...string) ([]byte, error) { + return []byte("mock output"), nil + } + defer func() { runApkFunc = origRunApkFunc }() + tests := []struct { name string req request @@ -404,6 +424,12 @@ func TestHandleRequest_ErrorMessages(t *testing.T) { // Note: Actual apk command execution will fail in test environment (no apk available), // but validation should pass. func TestHandleRequest_SuccessPath(t *testing.T) { + origRunApkFunc := runApkFunc + runApkFunc = func(args ...string) ([]byte, error) { + return []byte("mock output"), nil + } + defer func() { runApkFunc = origRunApkFunc }() + tests := []struct { action string pkg string @@ -438,6 +464,12 @@ func TestHandleRequest_SuccessPath(t *testing.T) { // TestHandleRequest_UpgradeValidation verifies that the upgrade action uses // the stricter validApkName regex (lowercase only, no @, no /). func TestHandleRequest_UpgradeValidation(t *testing.T) { + origRunApkFunc := runApkFunc + runApkFunc = func(args ...string) ([]byte, error) { + return []byte("mock output"), nil + } + defer func() { runApkFunc = origRunApkFunc }() + // Valid names for upgrade (lowercase apk grammar) valid := []string{ "curl", @@ -462,6 +494,12 @@ func TestHandleRequest_UpgradeValidation(t *testing.T) { // TestHandleRequest_UpgradeInjectionPatterns verifies 5 injection patterns are rejected. func TestHandleRequest_UpgradeInjectionPatterns(t *testing.T) { + origRunApkFunc := runApkFunc + runApkFunc = func(args ...string) ([]byte, error) { + return []byte("mock output"), nil + } + defer func() { runApkFunc = origRunApkFunc }() + injections := []string{ "-malicious", // leading hyphen "pkg;evil", // semicolon @@ -486,6 +524,12 @@ func TestHandleRequest_UpgradeInjectionPatterns(t *testing.T) { // by legacy validPkgName for install/uninstall) is REJECTED by upgrade action // via the stricter validApkName. func TestHandleRequest_UpgradeRejectsLegacySymbols(t *testing.T) { + origRunApkFunc := runApkFunc + runApkFunc = func(args ...string) ([]byte, error) { + return []byte("mock output"), nil + } + defer func() { runApkFunc = origRunApkFunc }() + legacySymbols := []string{ "pkg@edge", // @ accepted by validPkgName, rejected by validApkName "@scope/pkg", // npm scoped — rejected by validApkName