agent-security-scanner-mcp 3.11.0 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/utils.js CHANGED
@@ -1,10 +1,11 @@
1
- import { execFileSync } from "child_process";
1
+ import { execFileSync, execFile } from "child_process";
2
2
  import { createHash } from "crypto";
3
3
  import { readFileSync, existsSync } from "fs";
4
4
  import { dirname, join, extname, basename } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { FIX_TEMPLATES } from './fix-patterns.js';
7
7
  import { getDaemonClient, shutdownDaemon } from './daemon-client.js';
8
+ import { resolvePythonCommand, pythonArgs } from './python.js';
8
9
  export { isTestFile } from './context.js';
9
10
 
10
11
  // Handle both ESM and CJS bundling (Smithery bundles to CJS)
@@ -50,14 +51,16 @@ export function detectLanguage(filePath) {
50
51
 
51
52
  // Detect which analysis engine is available
52
53
  export function detectEngineMode() {
54
+ const pyCmd = resolvePythonCommand();
55
+ const pyArgs = pythonArgs();
53
56
  try {
54
- execFileSync('python3', ['-c', 'import tree_sitter; print("ast")'], {
57
+ execFileSync(pyCmd, [...pyArgs, '-c', 'import tree_sitter; print("ast")'], {
55
58
  encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
56
59
  });
57
60
  return 'ast';
58
61
  } catch {
59
62
  try {
60
- execFileSync('python3', ['--version'], {
63
+ execFileSync(pyCmd, [...pyArgs, '--version'], {
61
64
  encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
62
65
  });
63
66
  return 'regex';
@@ -80,11 +83,12 @@ export function getEngineMode() {
80
83
  export function runAnalyzer(filePath, engine = 'auto') {
81
84
  try {
82
85
  const analyzerPath = join(__dirname, '..', 'analyzer.py');
83
- const args = [analyzerPath, filePath];
86
+ const pyCmd = resolvePythonCommand();
87
+ const args = [...pythonArgs(), analyzerPath, filePath];
84
88
  if (engine !== 'auto') {
85
89
  args.push('--engine', engine);
86
90
  }
87
- const result = execFileSync('python3', args, {
91
+ const result = execFileSync(pyCmd, args, {
88
92
  encoding: 'utf-8',
89
93
  timeout: 45000 // Increased to 45s to match daemon timeout
90
94
  });
@@ -94,17 +98,71 @@ export function runAnalyzer(filePath, engine = 'auto') {
94
98
  }
95
99
  }
96
100
 
97
- // Async analyzer — tries daemon first, falls back to sync execFileSync
98
- export async function runAnalyzerAsync(filePath, engine = 'auto') {
101
+ // Async analyzer — tries daemon first, falls back to async execFile.
102
+ // Accepts an optional AbortSignal to truly cancel in-flight work (kills child process).
103
+ export async function runAnalyzerAsync(filePath, engine = 'auto', signal) {
104
+ if (signal && signal.aborted) throw new DOMException('Analysis aborted', 'AbortError');
99
105
  try {
100
106
  const client = getDaemonClient();
101
107
  if (client.isAvailable) {
102
- return await client.analyze(filePath, engine);
108
+ const analyzePromise = client.analyze(filePath, engine);
109
+ if (signal) {
110
+ return await Promise.race([
111
+ analyzePromise,
112
+ new Promise((_, reject) => {
113
+ signal.addEventListener('abort', () =>
114
+ reject(new DOMException('Analysis aborted', 'AbortError')),
115
+ { once: true }
116
+ );
117
+ }),
118
+ ]);
119
+ }
120
+ return await analyzePromise;
103
121
  }
104
- } catch {
105
- // Daemon failed fall through to sync
122
+ } catch (err) {
123
+ if (err.name === 'AbortError') throw err;
124
+ // Daemon failed — fall through to async execFile
106
125
  }
107
- return runAnalyzer(filePath, engine);
126
+ if (signal && signal.aborted) throw new DOMException('Analysis aborted', 'AbortError');
127
+
128
+ // Async fallback with true cancellation — kill child process on abort
129
+ return new Promise((resolve, reject) => {
130
+ const analyzerPath = join(__dirname, '..', 'analyzer.py');
131
+ const pyCmd = resolvePythonCommand();
132
+ const args = [...pythonArgs(), analyzerPath, filePath];
133
+ if (engine !== 'auto') args.push('--engine', engine);
134
+
135
+ const child = execFile(pyCmd, args, { encoding: 'utf-8', timeout: 45000 }, (error, stdout) => {
136
+ if (error) {
137
+ if (error.killed || error.signal) {
138
+ reject(new DOMException('Analysis aborted', 'AbortError'));
139
+ } else {
140
+ resolve({ error: error.message });
141
+ }
142
+ return;
143
+ }
144
+ try {
145
+ resolve(JSON.parse(stdout));
146
+ } catch {
147
+ resolve({ error: 'Failed to parse analyzer output' });
148
+ }
149
+ });
150
+
151
+ if (signal) {
152
+ const onAbort = () => {
153
+ child.kill();
154
+ reject(new DOMException('Analysis aborted', 'AbortError'));
155
+ };
156
+ if (signal.aborted) {
157
+ child.kill();
158
+ reject(new DOMException('Analysis aborted', 'AbortError'));
159
+ return;
160
+ }
161
+ signal.addEventListener('abort', onAbort, { once: true });
162
+ // Clean up listener when child completes normally
163
+ child.on('exit', () => signal.removeEventListener('abort', onAbort));
164
+ }
165
+ });
108
166
  }
109
167
 
110
168
  // Async cross-file analyzer — tries daemon first, falls back to sync
@@ -214,7 +272,8 @@ export function runCrossFileAnalyzer(filePaths) {
214
272
  try {
215
273
  const analyzerPath = join(__dirname, '..', 'cross_file_analyzer.py');
216
274
  if (!existsSync(analyzerPath)) return [];
217
- const result = execFileSync('python3', [analyzerPath, ...filePaths], {
275
+ const pyCmd = resolvePythonCommand();
276
+ const result = execFileSync(pyCmd, [...pythonArgs(), analyzerPath, ...filePaths], {
218
277
  encoding: 'utf-8',
219
278
  timeout: 120000,
220
279
  maxBuffer: 10 * 1024 * 1024