From 6381325e112a4c534aab5237fe079fe5f19bb981 Mon Sep 17 00:00:00 2001 From: Subham Ray Date: Tue, 1 Sep 2026 23:42:13 +0530 Subject: [PATCH] fix(node): resolve global roots from developer home --- internal/detector/nodedist_global.go | 13 +++++--- internal/detector/nodedist_global_test.go | 38 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/internal/detector/nodedist_global.go b/internal/detector/nodedist_global.go index 114fe50a..1d243364 100644 --- a/internal/detector/nodedist_global.go +++ b/internal/detector/nodedist_global.go @@ -121,12 +121,15 @@ func pnpmGlobalHomes(exec executor.Executor, home string) []string { } // nodeHomeDir returns the user's home directory via the platform-appropriate -// environment variable. Uses the env rather than user.Current so that, under a -// root daemon delegating to a logged-in user, callers that pre-set HOME resolve -// the user's tree. +// source. Windows package-manager roots follow USERPROFILE; macOS and Linux +// roots follow the resolved developer rather than the service process. func nodeHomeDir(exec executor.Executor) string { - if exec.GOOS() == model.PlatformWindows { + switch exec.GOOS() { + case model.PlatformWindows: return exec.Getenv("USERPROFILE") + case model.PlatformDarwin, model.PlatformLinux: + return executor.ResolveHome(exec) + default: + return exec.Getenv("HOME") } - return exec.Getenv("HOME") } diff --git a/internal/detector/nodedist_global_test.go b/internal/detector/nodedist_global_test.go index b69793fd..10d98415 100644 --- a/internal/detector/nodedist_global_test.go +++ b/internal/detector/nodedist_global_test.go @@ -3,9 +3,11 @@ package detector import ( "context" "path/filepath" + "slices" "testing" "github.com/step-security/dev-machine-guard/internal/executor" + "github.com/step-security/dev-machine-guard/internal/model" "github.com/step-security/dev-machine-guard/internal/progress" ) @@ -41,6 +43,42 @@ func TestNodeGlobalRoots_PrefixOverride(t *testing.T) { } } +func TestNodeGlobalRoots_UsesLoggedInUserHome(t *testing.T) { + serviceHome := "/root" + userHome := "/home/testuser" + npmRoot := filepath.Join(userHome, ".npm-global", "lib", "node_modules") + pnpmRoot := filepath.Join(userHome, ".local", "share", "pnpm", "global", "5", "node_modules") + yarnRoot := filepath.Join(userHome, ".config", "yarn", "global", "node_modules") + serviceNPMRoot := filepath.Join(serviceHome, ".npm-global", "lib", "node_modules") + servicePNPMRoot := filepath.Join(serviceHome, ".local", "share", "pnpm", "global", "5", "node_modules") + serviceYarnRoot := filepath.Join(serviceHome, ".config", "yarn", "global", "node_modules") + want := []nodeGlobalRoot{ + {pm: "npm", dir: npmRoot}, + {pm: "pnpm", dir: pnpmRoot}, + {pm: "yarn", dir: yarnRoot}, + } + mock := executor.NewMock() + mock.SetGOOS(model.PlatformLinux) + mock.SetEnv("HOME", serviceHome) + mock.SetHomeDir(userHome) + for _, dir := range []string{ + npmRoot, + pnpmRoot, + yarnRoot, + serviceNPMRoot, + servicePNPMRoot, + serviceYarnRoot, + } { + mock.SetDir(dir) + } + mock.SetGlob(filepath.Join(userHome, ".local", "share", "pnpm", "global", "*", "node_modules"), []string{pnpmRoot}) + mock.SetGlob(filepath.Join(serviceHome, ".local", "share", "pnpm", "global", "*", "node_modules"), []string{servicePNPMRoot}) + + if got := NodeGlobalRoots(mock); !slices.Equal(got, want) { + t.Fatalf("NodeGlobalRoots() = %+v, want %+v", got, want) + } +} + // Enterprise disk mode: ScanProjects emits structured packages with no raw // output and no package-manager invocation. func TestNodeScanner_DiskMode_Project(t *testing.T) {