Skip to content

Commit 38702ec

Browse files
authored
Merge pull request #16 from decksoftware/fix/semgrep-version-check-hang
fix(doctor): disable Semgrep's hanging version check (SEMGREP_ENABLE_VERSION_CHECK=0)
2 parents f58910f + 8db86ff commit 38702ec

2 files changed

Lines changed: 66 additions & 4 deletions

File tree

csreview/src/index.js

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ import { loadBaseline, applyBaseline, writeBaseline } from './baseline.js';
6464
*/
6565

6666
const 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;
6772
const WINDOWS_CMD_EXE = 'C:\\Windows\\System32\\cmd.exe';
6873
const TOOL_COMMANDS = new Set(['semgrep', 'npm', 'pnpm', 'bun', 'osv-scanner', 'python3', 'python']);
6974
const 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

840868
async 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

10641092
async 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',

csreview/test/tool-env.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// @ts-check
2+
import test from 'node:test';
3+
import assert from 'node:assert/strict';
4+
import { withToolEnv } from '../src/index.js';
5+
6+
// Semgrep's update/version "phone home" check can hang the process (notably on
7+
// Linux): `semgrep --version` prints the version then blocks on the check.
8+
// withToolEnv forces SEMGREP_ENABLE_VERSION_CHECK=0 for every semgrep invocation
9+
// (doctor version check, scan-path version check, and the scan itself).
10+
11+
test('withToolEnv forces SEMGREP_ENABLE_VERSION_CHECK=0 for semgrep and preserves the base env', () => {
12+
const opts = withToolEnv('semgrep', {}, { PATH: '/usr/bin' });
13+
assert.equal(opts.env.SEMGREP_ENABLE_VERSION_CHECK, '0');
14+
assert.equal(opts.env.PATH, '/usr/bin');
15+
});
16+
17+
test('withToolEnv respects an explicit user-set SEMGREP_ENABLE_VERSION_CHECK', () => {
18+
const opts = withToolEnv('semgrep', {}, { SEMGREP_ENABLE_VERSION_CHECK: '1', PATH: '/x' });
19+
assert.equal(opts.env.SEMGREP_ENABLE_VERSION_CHECK, '1');
20+
});
21+
22+
test('withToolEnv preserves caller options and lets a caller-provided env win', () => {
23+
const opts = withToolEnv('semgrep', { timeout: 10000, env: { FOO: 'bar' } }, { PATH: '/x' });
24+
assert.equal(opts.timeout, 10000);
25+
assert.equal(opts.env.FOO, 'bar');
26+
assert.equal(opts.env.SEMGREP_ENABLE_VERSION_CHECK, '0');
27+
});
28+
29+
test('withToolEnv is a no-op for non-semgrep tools (returns options unchanged)', () => {
30+
const original = { timeout: 5000 };
31+
const opts = withToolEnv('npm', original, { PATH: '/x' });
32+
assert.equal(opts, original);
33+
assert.equal(opts.env, undefined);
34+
});

0 commit comments

Comments
 (0)