Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions internal/detector/nodedist_global.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
38 changes: 38 additions & 0 deletions internal/detector/nodedist_global_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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) {
Expand Down
Loading