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/README.md +2 -2
- package/index.js +32 -14
- package/openclaw.plugin.json +3 -3
- package/package.json +2 -1
- package/src/cli/audit.js +10 -3
- package/src/cli/demo.js +3 -13
- package/src/cli/doctor.js +15 -13
- package/src/cli/harden.js +10 -3
- package/src/cli/init.js +11 -5
- package/src/config.js +4 -1
- package/src/daemon-client.js +3 -1
- package/src/python.js +54 -0
- package/src/tools/scan-action.js +222 -2
- package/src/tools/scan-prompt.js +34 -0
- package/src/tools/scan-skill.js +438 -80
- package/src/utils.js +71 -12
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (err.name === 'AbortError') throw err;
|
|
124
|
+
// Daemon failed — fall through to async execFile
|
|
106
125
|
}
|
|
107
|
-
|
|
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
|
|
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
|