@@ -3,6 +3,7 @@ package detector
33import (
44 "context"
55 "encoding/base64"
6+ "fmt"
67 "os"
78 "path/filepath"
89 "sort"
@@ -30,12 +31,70 @@ func getMaxProjectScanBytes() int64 {
3031
3132// NodeScanner performs enterprise-mode node scanning (raw output, base64 encoded).
3233type NodeScanner struct {
33- exec executor.Executor
34- log * progress.Logger
34+ exec executor.Executor
35+ log * progress.Logger
36+ loggedInUser string // when non-empty and running as root, commands run as this user
3537}
3638
37- func NewNodeScanner (exec executor.Executor , log * progress.Logger ) * NodeScanner {
38- return & NodeScanner {exec : exec , log : log }
39+ func NewNodeScanner (exec executor.Executor , log * progress.Logger , loggedInUser string ) * NodeScanner {
40+ return & NodeScanner {exec : exec , log : log , loggedInUser : loggedInUser }
41+ }
42+
43+ // shouldRunAsUser returns true when commands should be delegated to the logged-in user.
44+ func (s * NodeScanner ) shouldRunAsUser () bool {
45+ return s .exec .IsRoot () && s .loggedInUser != ""
46+ }
47+
48+ // runCmd runs a command, delegating to the logged-in user when running as root.
49+ // This ensures package manager commands use the real user's PATH and config.
50+ func (s * NodeScanner ) runCmd (ctx context.Context , timeout time.Duration , name string , args ... string ) (string , string , int , error ) {
51+ if s .shouldRunAsUser () {
52+ ctx , cancel := context .WithTimeout (ctx , timeout )
53+ defer cancel ()
54+ cmd := name
55+ for _ , a := range args {
56+ cmd += " " + a
57+ }
58+ stdout , err := s .exec .RunAsUser (ctx , s .loggedInUser , cmd )
59+ if err != nil {
60+ if ctx .Err () == context .DeadlineExceeded {
61+ return stdout , "" , 124 , fmt .Errorf ("command timed out after %s" , timeout )
62+ }
63+ return stdout , "" , 1 , err
64+ }
65+ return stdout , "" , 0 , nil
66+ }
67+ return s .exec .RunWithTimeout (ctx , timeout , name , args ... )
68+ }
69+
70+ // runShellCmd runs a shell command string, delegating to the logged-in user when running as root.
71+ func (s * NodeScanner ) runShellCmd (ctx context.Context , timeout time.Duration , shellCmd string ) (string , string , int , error ) {
72+ if s .shouldRunAsUser () {
73+ ctx , cancel := context .WithTimeout (ctx , timeout )
74+ defer cancel ()
75+ stdout , err := s .exec .RunAsUser (ctx , s .loggedInUser , shellCmd )
76+ if err != nil {
77+ if ctx .Err () == context .DeadlineExceeded {
78+ return stdout , "" , 124 , fmt .Errorf ("command timed out after %s" , timeout )
79+ }
80+ return stdout , "" , 1 , err
81+ }
82+ return stdout , "" , 0 , nil
83+ }
84+ return s .exec .RunWithTimeout (ctx , timeout , "bash" , "-c" , shellCmd )
85+ }
86+
87+ // checkPath checks if a binary is available, using the logged-in user's PATH when running as root.
88+ func (s * NodeScanner ) checkPath (ctx context.Context , name string ) error {
89+ if s .shouldRunAsUser () {
90+ path , err := s .exec .RunAsUser (ctx , s .loggedInUser , "which " + name )
91+ if err != nil || path == "" {
92+ return fmt .Errorf ("%s not found in user PATH" , name )
93+ }
94+ return nil
95+ }
96+ _ , err := s .exec .LookPath (name )
97+ return err
3998}
4099
41100// ScanGlobalPackages runs npm/yarn/pnpm list -g and returns raw base64-encoded results.
@@ -61,7 +120,7 @@ func (s *NodeScanner) ScanGlobalPackages(ctx context.Context) []model.NodeScanRe
61120}
62121
63122func (s * NodeScanner ) scanNPMGlobal (ctx context.Context ) (model.NodeScanResult , bool ) {
64- if _ , err := s .exec . LookPath ( "npm" ); err != nil {
123+ if err := s .checkPath ( ctx , "npm" ); err != nil {
65124 return model.NodeScanResult {}, false
66125 }
67126
@@ -72,7 +131,7 @@ func (s *NodeScanner) scanNPMGlobal(ctx context.Context) (model.NodeScanResult,
72131 }
73132
74133 start := time .Now ()
75- stdout , stderr , exitCode , _ := s .exec . RunWithTimeout (ctx , 60 * time .Second , "npm" , "list" , "-g" , "--json" , "--depth=3" )
134+ stdout , stderr , exitCode , _ := s .runCmd (ctx , 60 * time .Second , "npm" , "list" , "-g" , "--json" , "--depth=3" )
76135 duration := time .Since (start ).Milliseconds ()
77136
78137 errMsg := ""
@@ -94,7 +153,7 @@ func (s *NodeScanner) scanNPMGlobal(ctx context.Context) (model.NodeScanResult,
94153}
95154
96155func (s * NodeScanner ) scanYarnGlobal (ctx context.Context ) (model.NodeScanResult , bool ) {
97- if _ , err := s .exec . LookPath ( "yarn" ); err != nil {
156+ if err := s .checkPath ( ctx , "yarn" ); err != nil {
98157 return model.NodeScanResult {}, false
99158 }
100159
@@ -128,7 +187,7 @@ func (s *NodeScanner) scanYarnGlobal(ctx context.Context) (model.NodeScanResult,
128187}
129188
130189func (s * NodeScanner ) scanPnpmGlobal (ctx context.Context ) (model.NodeScanResult , bool ) {
131- if _ , err := s .exec . LookPath ( "pnpm" ); err != nil {
190+ if err := s .checkPath ( ctx , "pnpm" ); err != nil {
132191 return model.NodeScanResult {}, false
133192 }
134193
@@ -140,7 +199,7 @@ func (s *NodeScanner) scanPnpmGlobal(ctx context.Context) (model.NodeScanResult,
140199 globalDir = filepath .Dir (globalDir )
141200
142201 start := time .Now ()
143- stdout , stderr , exitCode , _ := s .exec . RunWithTimeout (ctx , 60 * time .Second , "pnpm" , "list" , "-g" , "--json" , "--depth=3" )
202+ stdout , stderr , exitCode , _ := s .runCmd (ctx , 60 * time .Second , "pnpm" , "list" , "-g" , "--json" , "--depth=3" )
144203 duration := time .Since (start ).Milliseconds ()
145204
146205 errMsg := ""
@@ -308,15 +367,15 @@ func (s *NodeScanner) scanProject(ctx context.Context, projectDir string) model.
308367}
309368
310369func (s * NodeScanner ) getVersion (ctx context.Context , binary , flag string ) string {
311- stdout , _ , _ , err := s .exec . RunWithTimeout (ctx , 10 * time .Second , binary , flag )
370+ stdout , _ , _ , err := s .runCmd (ctx , 10 * time .Second , binary , flag )
312371 if err != nil {
313372 return "unknown"
314373 }
315374 return strings .TrimSpace (stdout )
316375}
317376
318377func (s * NodeScanner ) getOutput (ctx context.Context , binary string , args ... string ) string {
319- stdout , _ , _ , err := s .exec . RunWithTimeout (ctx , 10 * time .Second , binary , args ... )
378+ stdout , _ , _ , err := s .runCmd (ctx , 10 * time .Second , binary , args ... )
320379 if err != nil {
321380 return ""
322381 }
0 commit comments