Skip to content

Commit 30ed51d

Browse files
authored
Merge pull request step-security#82 from Prateek-stepsecurity/pn/fix/pnpm
fix(pnpm): resolve v11 global-scan regression
2 parents 6b34f69 + 35d905d commit 30ed51d

2 files changed

Lines changed: 243 additions & 108 deletions

File tree

internal/detector/nodescan.go

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -199,27 +199,73 @@ func (s *NodeScanner) scanYarnGlobal(ctx context.Context) (model.NodeScanResult,
199199

200200
func (s *NodeScanner) scanPnpmGlobal(ctx context.Context) (model.NodeScanResult, bool) {
201201
if err := s.checkPath(ctx, "pnpm"); err != nil {
202+
s.log.Warn("pnpm not found on PATH — skipping pnpm global scan: %v", err)
202203
return model.NodeScanResult{}, false
203204
}
204205

205-
version := s.getVersion(ctx, "pnpm", "--version")
206-
globalDir := s.getOutput(ctx, "pnpm", "root", "-g")
207-
if globalDir == "" {
206+
pnpmCmd := "pnpm"
207+
208+
versionOut, _, _, verErr := s.runCmd(ctx, 10*time.Second, pnpmCmd, "--version")
209+
version := strings.TrimSpace(versionOut)
210+
if verErr != nil || version == "" {
211+
version = "unknown"
212+
}
213+
214+
rootOut, _, rootExit, _ := s.runCmd(ctx, 10*time.Second, pnpmCmd, "root", "-g")
215+
globalDir := strings.TrimSpace(rootOut)
216+
217+
// fallback logic in case `pnpm root -g` returns empty
218+
var extra string
219+
if rootExit != 0 || globalDir == "" {
220+
extra = defaultPnpmBinDir(s.exec)
221+
if extra != "" {
222+
oldPath := os.Getenv("PATH")
223+
_ = os.Setenv("PATH", extra+string(os.PathListSeparator)+oldPath)
224+
defer os.Setenv("PATH", oldPath)
225+
226+
// For the delegation path, embed `PATH='extra':$PATH` in the command name.
227+
// runCmd's delegation branch space-joins name+args into the shell command
228+
// string, so the env-prefix flows through to the user's shell intact.
229+
if s.shouldRunAsUser() {
230+
pnpmCmd = "PATH=" + platformShellQuote(s.exec, extra) + ":$PATH pnpm"
231+
}
232+
233+
s.log.Debug("pnpm root -g returned empty; retrying with bin dir %q prepended to PATH", extra)
234+
rootOut, _, _, _ = s.runCmd(ctx, 10*time.Second, pnpmCmd, "root", "-g")
235+
globalDir = strings.TrimSpace(rootOut)
236+
}
237+
}
238+
239+
if globalDir != "" {
240+
globalDir = filepath.Dir(globalDir)
241+
} else if extra != "" {
242+
// Both attempts failed; use the bin dir itself as last-resort
243+
// ProjectPath so we still produce a result rather than dropping the
244+
// scan entirely.
245+
s.log.Debug("pnpm root -g still empty after PATH workaround; using defaultPnpmBinDir=%q", extra)
246+
globalDir = extra
247+
} else {
248+
s.log.Warn("pnpm found but `pnpm root -g` returned empty and no fallback available — skipping pnpm global scan")
208249
return model.NodeScanResult{}, false
209250
}
210-
globalDir = filepath.Dir(globalDir)
211251

252+
// Try with --depth=3 first for transitive coverage (works on pnpm v10).
253+
// Fall back to no --depth on non-zero exit — pnpm v11 hard-fails any
254+
// --depth>=1 on -g and pnpm itself recommends omitting --depth.
212255
start := time.Now()
213-
// pnpm v11 exits non-zero on `--depth=3` for global scans; use `--depth=Infinity` there.
214-
stdout, stderr, exitCode, _ := s.runCmd(ctx, 60*time.Second, "pnpm", "list", "-g", "--json", pnpmDepthArg(version))
256+
stdout, stderr, exitCode, err := s.runCmd(ctx, 60*time.Second, pnpmCmd, "list", "-g", "--json", "--depth=3")
257+
if exitCode != 0 {
258+
s.log.Debug("pnpm list -g --depth=3 failed (exit=%d) — retrying without --depth (v11 path)", exitCode)
259+
stdout, stderr, exitCode, err = s.runCmd(ctx, 60*time.Second, pnpmCmd, "list", "-g", "--json")
260+
}
215261
duration := time.Since(start).Milliseconds()
216262

217263
errMsg := ""
218264
if exitCode != 0 {
219265
errMsg = "pnpm list -g command failed"
220266
s.log.Warn("pnpm list -g failed (exit_code=%d, %dms) — results may be incomplete", exitCode, duration)
221267
}
222-
s.log.Debug("pnpm global scan: version=%s global_dir=%s exit_code=%d stdout_bytes=%d duration=%dms", version, globalDir, exitCode, len(stdout), duration)
268+
s.log.Debug("pnpm global scan: version=%s global_dir=%s exit_code=%d stdout_bytes=%d duration=%dms err=%v", version, globalDir, exitCode, len(stdout), duration, err)
223269

224270
return model.NodeScanResult{
225271
ProjectPath: globalDir,
@@ -234,6 +280,26 @@ func (s *NodeScanner) scanPnpmGlobal(ctx context.Context) (model.NodeScanResult,
234280
}, true
235281
}
236282

283+
// defaultPnpmBinDir returns the default pnpm global bin directory for the current OS
284+
// based on environment variables.
285+
func defaultPnpmBinDir(exec executor.Executor) string {
286+
switch exec.GOOS() {
287+
case model.PlatformDarwin:
288+
if home := exec.Getenv("HOME"); home != "" {
289+
return filepath.Join(home, "Library", "pnpm", "bin")
290+
}
291+
case model.PlatformLinux:
292+
if home := exec.Getenv("HOME"); home != "" {
293+
return filepath.Join(home, ".local", "share", "pnpm")
294+
}
295+
case model.PlatformWindows:
296+
if localAppData := exec.Getenv("LOCALAPPDATA"); localAppData != "" {
297+
return filepath.Join(localAppData, "pnpm")
298+
}
299+
}
300+
return ""
301+
}
302+
237303
// projectEntry holds a discovered package.json with its modification time for sorting.
238304
type projectEntry struct {
239305
dir string
@@ -405,19 +471,3 @@ func isInsideNodeModules(projectDir string) bool {
405471
normalized := strings.ReplaceAll(projectDir, "\\", "/")
406472
return strings.Contains(normalized, "/node_modules/")
407473
}
408-
409-
// pnpmDepthArg picks the `--depth` arg for `pnpm list -g` based on the pnpm
410-
// version. pnpm v11 exits non-zero when `--depth=3` is passed to a global
411-
// scan; `--depth=Infinity` works on v11 and preserves transitive depth.
412-
// Falls back to `--depth=3` for older / unparseable versions to preserve
413-
// existing behavior.
414-
func pnpmDepthArg(version string) string {
415-
v := strings.TrimSpace(version)
416-
v = strings.TrimPrefix(v, "v")
417-
major, _, _ := strings.Cut(v, ".")
418-
n, err := strconv.Atoi(major)
419-
if err == nil && n >= 11 {
420-
return "--depth=Infinity"
421-
}
422-
return "--depth=3"
423-
}

0 commit comments

Comments
 (0)