@@ -64,6 +64,11 @@ import { loadBaseline, applyBaseline, writeBaseline } from './baseline.js';
6464 */
6565
6666const execFileAsync = promisify ( execFile ) ;
67+
68+ // `--version` and other quick tool probes should return near-instantly; cap them
69+ // so a misbehaving tool (e.g. Semgrep's hanging version check) can never block a
70+ // scan or the doctor.
71+ const VERSION_CHECK_TIMEOUT_MS = 10000 ;
6772const WINDOWS_CMD_EXE = 'C:\\Windows\\System32\\cmd.exe' ;
6873const TOOL_COMMANDS = new Set ( [ 'semgrep' , 'npm' , 'pnpm' , 'bun' , 'osv-scanner' , 'python3' , 'python' ] ) ;
6974const WINDOWS_CMD_META_CHARS = / [ \r \n & | ^ < > " % ] / ;
@@ -78,6 +83,28 @@ function executable(command) {
7883 return command ;
7984}
8085
86+ /**
87+ * Build exec options for a tool, disabling Semgrep's update/version "phone home"
88+ * check — it can hang the process (notably on Linux, where `semgrep --version`
89+ * prints the version and then blocks on the check). Forced off for every semgrep
90+ * invocation unless the user set SEMGREP_ENABLE_VERSION_CHECK explicitly. No-op
91+ * for other tools (returns the options object unchanged).
92+ *
93+ * @param {string } command
94+ * @param {import('child_process').ExecFileOptions } [options]
95+ * @param {NodeJS.ProcessEnv } [baseEnv]
96+ * @returns {import('child_process').ExecFileOptions }
97+ */
98+ export function withToolEnv ( command , options = { } , baseEnv = process . env ) {
99+ if ( command !== 'semgrep' ) return options ;
100+ return {
101+ ...options ,
102+ // `'0'` first so it is the default, but baseEnv (an explicit user setting)
103+ // overrides it, and a caller-provided options.env wins last.
104+ env : { SEMGREP_ENABLE_VERSION_CHECK : '0' , ...baseEnv , ...( options . env || { } ) } ,
105+ } ;
106+ }
107+
81108/**
82109 * Execute an allowlisted external tool without a shell except for Windows npm-style
83110 * .cmd shims, which require cmd.exe and are guarded against shell metacharacters.
@@ -91,15 +118,16 @@ function execTool(command, args, options = {}) {
91118 const commandPath = executable ( command ) ;
92119 const needsShell = process . platform === 'win32' && commandPath . endsWith ( '.cmd' ) ;
93120 validateWindowsCmdArgs ( commandPath , args ) ;
121+ const execOptions = withToolEnv ( command , options ) ;
94122 if ( needsShell ) {
95123 // Invariant: .cmd tools execute through cmd.exe on Windows, so user-controlled
96124 // values such as rootDir, targets, agentName, and paths must never be passed
97125 // in args for .cmd commands. User input belongs in cwd or direct non-.cmd tools.
98126 return /** @type {Promise<{stdout: string, stderr: string}> } */ (
99- execFileAsync ( WINDOWS_CMD_EXE , [ '/d' , '/s' , '/c' , commandPath , ...args ] , options )
127+ execFileAsync ( WINDOWS_CMD_EXE , [ '/d' , '/s' , '/c' , commandPath , ...args ] , execOptions )
100128 ) ;
101129 }
102- return /** @type {Promise<{stdout: string, stderr: string}> } */ ( execFileAsync ( commandPath , args , options ) ) ;
130+ return /** @type {Promise<{stdout: string, stderr: string}> } */ ( execFileAsync ( commandPath , args , execOptions ) ) ;
103131}
104132
105133/**
@@ -839,7 +867,7 @@ export function normalizeOsvScannerFindings(osvJson = {}, rootDir = process.cwd(
839867
840868async function runSemgrep ( rootDir ) {
841869 try {
842- const versionResult = await execTool ( 'semgrep' , [ '--version' ] , { timeout : 30000 } ) ;
870+ const versionResult = await execTool ( 'semgrep' , [ '--version' ] , { timeout : VERSION_CHECK_TIMEOUT_MS } ) ;
843871 const scanResult = await execTool (
844872 'semgrep' ,
845873 [ '--config' , 'auto' , '--json' , '--quiet' , '--exclude' , 'node_modules' , '--exclude' , 'csreview-reports' , rootDir ] ,
@@ -1063,7 +1091,7 @@ async function runOsvScanner(rootDir) {
10631091
10641092async function checkToolVersion ( command , args = [ '--version' ] ) {
10651093 try {
1066- const result = await execTool ( command , args , { timeout : 30000 } ) ;
1094+ const result = await execTool ( command , args , { timeout : VERSION_CHECK_TIMEOUT_MS } ) ;
10671095 return {
10681096 available : true ,
10691097 version : result . stdout . trim ( ) || result . stderr . trim ( ) || 'version unknown' ,
0 commit comments