agent-security-scanner-mcp 3.7.0 → 3.9.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/daemon.py ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+ """JSONL daemon wrapping analyzer.py for persistent process reuse.
3
+
4
+ Protocol: One JSON object per line over stdin/stdout. stderr for debug logs only.
5
+ Startup: sends {"id":"__ready__","success":true,"result":{"status":"ready"}}
6
+ Actions: analyze, cross_file_analyze, health, shutdown
7
+ """
8
+
9
+ import sys
10
+ import os
11
+
12
+ # CRITICAL: Redirect stdout to stderr BEFORE any imports can print to stdout.
13
+ # This prevents any imported library from corrupting the JSONL protocol channel.
14
+ _protocol_stdout = sys.stdout
15
+ sys.stdout = sys.stderr
16
+
17
+ # Now safe to import everything
18
+ import json
19
+ import time
20
+ from collections import OrderedDict
21
+
22
+ # Add script directory to path so analyzer imports work
23
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
24
+
25
+ from analyzer import analyze_file, analyze_file_ast, analyze_file_regex
26
+
27
+ try:
28
+ from cross_file_analyzer import cross_file_analyze
29
+ HAS_CROSS_FILE = True
30
+ except ImportError:
31
+ HAS_CROSS_FILE = False
32
+
33
+
34
+ class LRUCache:
35
+ """Simple LRU cache keyed by (file_path, mtime), capped at max_size entries."""
36
+
37
+ def __init__(self, max_size=200):
38
+ self._cache = OrderedDict()
39
+ self._max_size = max_size
40
+
41
+ def get(self, file_path):
42
+ try:
43
+ mtime = os.path.getmtime(file_path)
44
+ except OSError:
45
+ return None
46
+ key = (file_path, mtime)
47
+ if key in self._cache:
48
+ self._cache.move_to_end(key)
49
+ return self._cache[key]
50
+ return None
51
+
52
+ def put(self, file_path, result):
53
+ try:
54
+ mtime = os.path.getmtime(file_path)
55
+ except OSError:
56
+ return
57
+ key = (file_path, mtime)
58
+ self._cache[key] = result
59
+ self._cache.move_to_end(key)
60
+ while len(self._cache) > self._max_size:
61
+ self._cache.popitem(last=False)
62
+
63
+ @property
64
+ def size(self):
65
+ return len(self._cache)
66
+
67
+
68
+ _cache = LRUCache(max_size=200)
69
+
70
+
71
+ def send_response(obj):
72
+ """Write a JSON line to the protocol channel (original stdout)."""
73
+ line = json.dumps(obj, separators=(',', ':'))
74
+ _protocol_stdout.write(line + '\n')
75
+ _protocol_stdout.flush()
76
+
77
+
78
+ def handle_analyze(req):
79
+ file_path = req.get('file_path')
80
+ engine = req.get('engine', 'auto')
81
+
82
+ if not file_path or not os.path.exists(file_path):
83
+ return {'success': False, 'error': f'File not found: {file_path}'}
84
+
85
+ # Check cache (only for engine=auto)
86
+ if engine == 'auto':
87
+ cached = _cache.get(file_path)
88
+ if cached is not None:
89
+ return {'success': True, 'result': cached, 'cached': True, 'cache_size': _cache.size}
90
+
91
+ try:
92
+ if engine == 'regex':
93
+ result = analyze_file_regex(file_path)
94
+ elif engine == 'ast':
95
+ result = analyze_file_ast(file_path)
96
+ else:
97
+ result = analyze_file(file_path)
98
+ except Exception as e:
99
+ return {'success': False, 'error': str(e)}
100
+
101
+ if isinstance(result, dict) and 'error' in result:
102
+ return {'success': False, 'error': result['error']}
103
+
104
+ # Cache result (only for engine=auto)
105
+ if engine == 'auto':
106
+ _cache.put(file_path, result)
107
+
108
+ return {'success': True, 'result': result, 'cached': False, 'cache_size': _cache.size}
109
+
110
+
111
+ def handle_cross_file_analyze(req):
112
+ file_paths = req.get('file_paths', [])
113
+ if not file_paths:
114
+ return {'success': False, 'error': 'No file_paths provided'}
115
+ if not HAS_CROSS_FILE:
116
+ return {'success': False, 'error': 'cross_file_analyzer not available'}
117
+
118
+ try:
119
+ result = cross_file_analyze(file_paths)
120
+ return {'success': True, 'result': result}
121
+ except Exception as e:
122
+ return {'success': False, 'error': str(e)}
123
+
124
+
125
+ def handle_health():
126
+ return {
127
+ 'success': True,
128
+ 'result': {
129
+ 'status': 'healthy',
130
+ 'cache_size': _cache.size,
131
+ 'pid': os.getpid(),
132
+ 'uptime': time.time() - _start_time,
133
+ }
134
+ }
135
+
136
+
137
+ def main():
138
+ global _start_time
139
+ _start_time = time.time()
140
+
141
+ # Signal readiness
142
+ send_response({
143
+ 'id': '__ready__',
144
+ 'success': True,
145
+ 'result': {'status': 'ready'}
146
+ })
147
+
148
+ for line in sys.stdin:
149
+ line = line.strip()
150
+ if not line:
151
+ continue
152
+
153
+ try:
154
+ req = json.loads(line)
155
+ except json.JSONDecodeError as e:
156
+ send_response({'id': None, 'success': False, 'error': f'Invalid JSON: {e}'})
157
+ continue
158
+
159
+ req_id = req.get('id')
160
+ action = req.get('action')
161
+
162
+ if action == 'shutdown':
163
+ send_response({'id': req_id, 'success': True, 'result': {'status': 'shutdown'}})
164
+ break
165
+ elif action == 'health':
166
+ resp = handle_health()
167
+ elif action == 'analyze':
168
+ resp = handle_analyze(req)
169
+ elif action == 'cross_file_analyze':
170
+ resp = handle_cross_file_analyze(req)
171
+ else:
172
+ resp = {'success': False, 'error': f'Unknown action: {action}'}
173
+
174
+ resp['id'] = req_id
175
+ send_response(resp)
176
+
177
+
178
+ if __name__ == '__main__':
179
+ main()
package/index.js CHANGED
@@ -11,15 +11,21 @@ import { homedir, platform } from "os";
11
11
  import { createInterface } from "readline";
12
12
  import { createHash } from "crypto";
13
13
  import { envVarReplacement, FIX_TEMPLATES } from './src/fix-patterns.js';
14
- import { detectLanguage, runAnalyzer, generateFix, toSarif } from './src/utils.js';
14
+ import { detectLanguage, runAnalyzer, generateFix, toSarif, shutdownDaemon } from './src/utils.js';
15
15
  import { scanSecuritySchema, scanSecurity } from './src/tools/scan-security.js';
16
16
  import { fixSecuritySchema, fixSecurity } from './src/tools/fix-security.js';
17
17
  import { loadPackageLists, checkPackageSchema, checkPackage, getPackageStats } from './src/tools/check-package.js';
18
18
  import { scanPackagesSchema, scanPackages } from './src/tools/scan-packages.js';
19
19
  import { scanAgentPromptSchema, scanAgentPrompt } from './src/tools/scan-prompt.js';
20
+ import { scanDiffSchema, scanDiff } from './src/tools/scan-diff.js';
21
+ import { scanProjectSchema, scanProject } from './src/tools/scan-project.js';
22
+ import { scanAgentActionSchema, scanAgentAction } from './src/tools/scan-action.js';
23
+ import { scanMcpServerSchema, scanMcpServer } from './src/tools/scan-mcp.js';
20
24
  import { runInit } from './src/cli/init.js';
21
25
  import { runDoctor } from './src/cli/doctor.js';
22
26
  import { runDemo } from './src/cli/demo.js';
27
+ import { runInitHooks } from './src/cli/init-hooks.js';
28
+ import { runReport } from './src/cli/report.js';
23
29
 
24
30
  // Handle both ESM and CJS bundling (Smithery bundles to CJS)
25
31
  let __dirname;
@@ -134,6 +140,46 @@ server.tool(
134
140
  scanAgentPrompt
135
141
  );
136
142
 
143
+ // Register scan_git_diff tool
144
+ server.tool(
145
+ "scan_git_diff",
146
+ "Scan git diff for new security vulnerabilities. Only reports issues on changed lines. Use for PR reviews.",
147
+ scanDiffSchema,
148
+ scanDiff
149
+ );
150
+
151
+ // Register scan_project tool
152
+ server.tool(
153
+ "scan_project",
154
+ "Scan an entire directory for security vulnerabilities with .gitignore support and security grading. Use verbosity='minimal' for grade + counts, 'compact' (default) for top issues, 'full' for all details.",
155
+ scanProjectSchema,
156
+ scanProject
157
+ );
158
+
159
+ // ===========================================
160
+ // AGENT ACTION MONITORING
161
+ // ===========================================
162
+
163
+ // Register scan_agent_action tool
164
+ server.tool(
165
+ "scan_agent_action",
166
+ "Pre-execution security check for agent actions (bash, file_write, file_read, http_request, file_delete). Returns ALLOW/WARN/BLOCK. Lighter than scan_agent_prompt — evaluates concrete actions.",
167
+ scanAgentActionSchema,
168
+ scanAgentAction
169
+ );
170
+
171
+ // ===========================================
172
+ // MCP SERVER SECURITY SCANNING
173
+ // ===========================================
174
+
175
+ // Register scan_mcp_server tool
176
+ server.tool(
177
+ "scan_mcp_server",
178
+ "Scan an MCP server's source code for security vulnerabilities: overly broad permissions, missing input validation, data exfiltration, insecure patterns. Returns grade (A-F) and recommendations.",
179
+ scanMcpServerSchema,
180
+ scanMcpServer
181
+ );
182
+
137
183
  // ===========================================
138
184
  // CLI COMMANDS - Extracted to src/cli/
139
185
  // ===========================================
@@ -156,17 +202,234 @@ if (cliArgs[0] === 'init') {
156
202
  console.error(` Error: ${err.message}\n`);
157
203
  process.exit(1);
158
204
  });
205
+ } else if (cliArgs[0] === 'init-hooks') {
206
+ runInitHooks(cliArgs.slice(1)).then(() => process.exit(0)).catch((err) => {
207
+ console.error(` Error: ${err.message}\n`);
208
+ process.exit(1);
209
+ });
210
+ } else if (cliArgs[0] === 'report') {
211
+ runReport(cliArgs.slice(1)).then(() => process.exit(0)).catch((err) => {
212
+ console.error(` Error: ${err.message}\n`);
213
+ process.exit(1);
214
+ });
215
+ } else if (cliArgs[0] === 'scan-prompt') {
216
+ // CLI mode: scan-prompt <text> [--verbosity minimal|compact|full]
217
+ const text = cliArgs[1];
218
+ if (!text) {
219
+ console.error('Usage: agent-security-scanner-mcp scan-prompt <text> [--verbosity minimal|compact|full]');
220
+ process.exit(1);
221
+ }
222
+ const verbosityIdx = cliArgs.indexOf('--verbosity');
223
+ const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
224
+
225
+ loadPackageLists();
226
+ scanAgentPrompt({ prompt_text: text, verbosity }).then(result => {
227
+ const output = JSON.parse(result.content[0].text);
228
+ console.log(JSON.stringify(output, null, 2));
229
+ process.exit(output.action === 'BLOCK' ? 1 : 0);
230
+ }).catch(err => {
231
+ console.error(JSON.stringify({ error: err.message }));
232
+ process.exit(1);
233
+ });
234
+ } else if (cliArgs[0] === 'scan-security') {
235
+ // CLI mode: scan-security <file> [--verbosity minimal|compact|full] [--format json|sarif]
236
+ const filePath = cliArgs[1];
237
+ if (!filePath) {
238
+ console.error('Usage: agent-security-scanner-mcp scan-security <file> [--verbosity minimal|compact|full] [--format json|sarif]');
239
+ process.exit(1);
240
+ }
241
+ const verbosityIdx = cliArgs.indexOf('--verbosity');
242
+ const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
243
+ const formatIdx = cliArgs.indexOf('--format');
244
+ const outputFormat = formatIdx !== -1 ? cliArgs[formatIdx + 1] : 'json';
245
+
246
+ loadPackageLists();
247
+ scanSecurity({ file_path: filePath, verbosity, output_format: outputFormat }).then(result => {
248
+ const output = JSON.parse(result.content[0].text);
249
+ console.log(JSON.stringify(output, null, 2));
250
+ process.exit(output.issues_count > 0 || output.total > 0 ? 1 : 0);
251
+ }).catch(err => {
252
+ console.error(JSON.stringify({ error: err.message }));
253
+ process.exit(1);
254
+ });
255
+ } else if (cliArgs[0] === 'check-package') {
256
+ // CLI mode: check-package <name> <ecosystem>
257
+ const packageName = cliArgs[1];
258
+ const ecosystem = cliArgs[2];
259
+ if (!packageName || !ecosystem) {
260
+ console.error('Usage: agent-security-scanner-mcp check-package <name> <ecosystem>');
261
+ console.error('Ecosystems: npm, pypi, rubygems, crates, dart, perl, raku');
262
+ process.exit(1);
263
+ }
264
+
265
+ loadPackageLists();
266
+ checkPackage({ package_name: packageName, ecosystem }).then(result => {
267
+ const output = JSON.parse(result.content[0].text);
268
+ console.log(JSON.stringify(output, null, 2));
269
+ process.exit(output.legitimate ? 0 : 1);
270
+ }).catch(err => {
271
+ console.error(JSON.stringify({ error: err.message }));
272
+ process.exit(1);
273
+ });
274
+ } else if (cliArgs[0] === 'scan-packages') {
275
+ // CLI mode: scan-packages <file> <ecosystem> [--verbosity minimal|compact|full]
276
+ const filePath = cliArgs[1];
277
+ const ecosystem = cliArgs[2];
278
+ if (!filePath || !ecosystem) {
279
+ console.error('Usage: agent-security-scanner-mcp scan-packages <file> <ecosystem> [--verbosity minimal|compact|full]');
280
+ console.error('Ecosystems: npm, pypi, rubygems, crates, dart, perl, raku');
281
+ process.exit(1);
282
+ }
283
+ const verbosityIdx = cliArgs.indexOf('--verbosity');
284
+ const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
285
+
286
+ loadPackageLists();
287
+ scanPackages({ file_path: filePath, ecosystem, verbosity }).then(result => {
288
+ const output = JSON.parse(result.content[0].text);
289
+ console.log(JSON.stringify(output, null, 2));
290
+ process.exit(output.hallucinated_count > 0 ? 1 : 0);
291
+ }).catch(err => {
292
+ console.error(JSON.stringify({ error: err.message }));
293
+ process.exit(1);
294
+ });
295
+ } else if (cliArgs[0] === 'scan-project') {
296
+ // CLI mode: scan-project <dir> [--recursive] [--diff-only] [--cross-file] [--include '*.py'] [--exclude '*.test.js'] [--verbosity minimal|compact|full]
297
+ const dirPath = cliArgs[1];
298
+ if (!dirPath || dirPath.startsWith('--')) {
299
+ console.error('Usage: agent-security-scanner-mcp scan-project <directory> [--recursive] [--diff-only] [--cross-file] [--include <pattern>] [--exclude <pattern>] [--verbosity minimal|compact|full]');
300
+ process.exit(1);
301
+ }
302
+ const verbosityIdx = cliArgs.indexOf('--verbosity');
303
+ const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
304
+ const recursive = !cliArgs.includes('--no-recursive');
305
+ const diffOnly = cliArgs.includes('--diff-only');
306
+ const crossFile = cliArgs.includes('--cross-file');
307
+ const includeIdx = cliArgs.indexOf('--include');
308
+ const includePatterns = includeIdx !== -1 ? [cliArgs[includeIdx + 1]] : undefined;
309
+ const excludeIdx = cliArgs.indexOf('--exclude');
310
+ const excludePatterns = excludeIdx !== -1 ? [cliArgs[excludeIdx + 1]] : undefined;
311
+
312
+ scanProject({ directory_path: dirPath, recursive, diff_only: diffOnly, cross_file: crossFile, include_patterns: includePatterns, exclude_patterns: excludePatterns, verbosity }).then(result => {
313
+ const output = JSON.parse(result.content[0].text);
314
+ console.log(JSON.stringify(output, null, 2));
315
+ const total = output.issues_count || output.total || 0;
316
+ process.exit(total > 0 ? 1 : 0);
317
+ }).catch(err => {
318
+ console.error(JSON.stringify({ error: err.message }));
319
+ process.exit(1);
320
+ });
321
+ } else if (cliArgs[0] === 'scan-diff') {
322
+ // CLI mode: scan-diff [base] [target] [--verbosity minimal|compact|full]
323
+ // Parse positional args, skipping flag values
324
+ const verbosityIdx = cliArgs.indexOf('--verbosity');
325
+ const flagValueIndices = new Set(verbosityIdx !== -1 ? [verbosityIdx, verbosityIdx + 1] : []);
326
+ const positionalArgs = cliArgs.slice(1).filter((arg, idx) => !arg.startsWith('--') && !flagValueIndices.has(idx + 1));
327
+ const baseRef = positionalArgs[0];
328
+ const targetRef = positionalArgs[1];
329
+ const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
330
+
331
+ scanDiff({ base_ref: baseRef, target_ref: targetRef, verbosity }).then(result => {
332
+ const output = JSON.parse(result.content[0].text);
333
+ console.log(JSON.stringify(output, null, 2));
334
+ process.exit(output.issues_count > 0 || output.total > 0 ? 1 : 0);
335
+ }).catch(err => {
336
+ console.error(JSON.stringify({ error: err.message }));
337
+ process.exit(1);
338
+ });
339
+ } else if (cliArgs[0] === 'benchmark') {
340
+ // CLI mode: benchmark [--save] [--json-only] [--compare-latest] [--corpus <path>]
341
+ const benchmarkPath = join(__dirname, 'benchmarks', 'benchmark_runner.py');
342
+ const benchArgs = [benchmarkPath];
343
+
344
+ // Pass through supported flags
345
+ for (let i = 1; i < cliArgs.length; i++) {
346
+ if (['--save', '--json-only', '--compare-latest'].includes(cliArgs[i])) {
347
+ benchArgs.push(cliArgs[i]);
348
+ } else if (cliArgs[i] === '--corpus' && cliArgs[i + 1]) {
349
+ benchArgs.push('--corpus', cliArgs[i + 1]);
350
+ i++;
351
+ }
352
+ }
353
+
354
+ try {
355
+ execFileSync('python3', benchArgs, { stdio: 'inherit', timeout: 300000 });
356
+ } catch (err) {
357
+ if (err.status) process.exit(err.status);
358
+ console.error(`Benchmark error: ${err.message}`);
359
+ process.exit(1);
360
+ }
361
+ process.exit(0);
362
+ } else if (cliArgs[0] === 'scan-mcp') {
363
+ // CLI mode: scan-mcp <path> [--verbosity minimal|compact|full]
364
+ const serverPath = cliArgs[1];
365
+ if (!serverPath) {
366
+ console.error('Usage: agent-security-scanner-mcp scan-mcp <server-path> [--verbosity minimal|compact|full]');
367
+ process.exit(1);
368
+ }
369
+ const verbosityIdx = cliArgs.indexOf('--verbosity');
370
+ const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
371
+
372
+ scanMcpServer({ server_path: serverPath, verbosity }).then(result => {
373
+ const output = JSON.parse(result.content[0].text);
374
+ console.log(JSON.stringify(output, null, 2));
375
+ process.exit(output.findings_count > 0 ? 1 : 0);
376
+ }).catch(err => {
377
+ console.error(JSON.stringify({ error: err.message }));
378
+ process.exit(1);
379
+ });
380
+ } else if (cliArgs[0] === 'scan-action') {
381
+ // CLI mode: scan-action <type> <value> [--verbosity minimal|compact|full]
382
+ const actionType = cliArgs[1];
383
+ const actionValue = cliArgs[2];
384
+ if (!actionType || !actionValue) {
385
+ console.error('Usage: agent-security-scanner-mcp scan-action <type> <value> [--verbosity minimal|compact|full]');
386
+ console.error('Types: bash, file_write, file_read, http_request, file_delete');
387
+ process.exit(1);
388
+ }
389
+ const verbosityIdx = cliArgs.indexOf('--verbosity');
390
+ const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
391
+
392
+ scanAgentAction({ action_type: actionType, action_value: actionValue, verbosity }).then(result => {
393
+ const output = JSON.parse(result.content[0].text);
394
+ console.log(JSON.stringify(output, null, 2));
395
+ process.exit(output.action === 'BLOCK' ? 1 : 0);
396
+ }).catch(err => {
397
+ console.error(JSON.stringify({ error: err.message }));
398
+ process.exit(1);
399
+ });
159
400
  } else if (cliArgs[0] === '--help' || cliArgs[0] === '-h' || cliArgs[0] === 'help') {
160
401
  console.log('\n agent-security-scanner-mcp\n');
161
402
  console.log(' Commands:');
162
403
  console.log(' init [client] Set up MCP config for a client');
404
+ console.log(' init-hooks Install Claude Code hooks for auto-scanning');
163
405
  console.log(' doctor [--fix] Check environment & client configs');
164
406
  console.log(' demo [--lang js] Generate vulnerable file + scan it');
407
+ console.log(' report <dir> Generate HTML security report with history');
408
+ console.log(' benchmark [flags] Run accuracy benchmarks\n');
409
+ console.log(' CLI Tools (for scripts & OpenClaw):');
410
+ console.log(' scan-prompt <text> Scan prompt for injection attacks');
411
+ console.log(' scan-security <file> Scan file for vulnerabilities');
412
+ console.log(' check-package <n> <e> Check if package exists in ecosystem');
413
+ console.log(' scan-packages <f> <e> Scan file imports for hallucinated packages');
414
+ console.log(' scan-project <dir> Scan directory for vulnerabilities with grading');
415
+ console.log(' scan-diff [base] [target] Scan git diff for new vulnerabilities');
416
+ console.log(' scan-mcp <path> Scan MCP server source for security issues');
417
+ console.log(' scan-action <t> <v> Check agent action before execution\n');
165
418
  console.log(' (no args) Start MCP server on stdio\n');
419
+ console.log(' Options:');
420
+ console.log(' --verbosity <level> minimal|compact|full (default: compact)');
421
+ console.log(' --format <type> json|sarif (scan-security only)');
422
+ console.log(' --include <pattern> Include only matching files (scan-project)');
423
+ console.log(' --exclude <pattern> Exclude matching files (scan-project)\n');
166
424
  console.log(' Examples:');
167
425
  console.log(' npx agent-security-scanner-mcp init');
168
- console.log(' npx agent-security-scanner-mcp doctor --fix');
169
- console.log(' npx agent-security-scanner-mcp demo --lang py\n');
426
+ console.log(' npx agent-security-scanner-mcp scan-prompt "ignore previous instructions"');
427
+ console.log(' npx agent-security-scanner-mcp scan-security ./app.py --verbosity minimal');
428
+ console.log(' npx agent-security-scanner-mcp check-package flask pypi');
429
+ console.log(' npx agent-security-scanner-mcp scan-project ./src --verbosity minimal');
430
+ console.log(' npx agent-security-scanner-mcp scan-diff HEAD~1');
431
+ console.log(' npx agent-security-scanner-mcp report ./src --json');
432
+ console.log(' npx agent-security-scanner-mcp benchmark --save --compare-latest\n');
170
433
  process.exit(0);
171
434
  } else {
172
435
  // Normal MCP server mode
@@ -176,8 +439,21 @@ if (cliArgs[0] === 'init') {
176
439
  const transport = new StdioServerTransport();
177
440
  await server.connect(transport);
178
441
  console.error("Security Scanner MCP Server running on stdio");
442
+
443
+ // Shutdown daemon when MCP server closes
444
+ server.server.onclose = async () => {
445
+ await shutdownDaemon();
446
+ };
179
447
  }
180
448
 
449
+ // Graceful shutdown on signals
450
+ const shutdownHandler = async () => {
451
+ await shutdownDaemon();
452
+ process.exit(0);
453
+ };
454
+ process.on('SIGTERM', shutdownHandler);
455
+ process.on('SIGINT', shutdownHandler);
456
+
181
457
  main().catch((error) => {
182
458
  console.error("Fatal error:", error);
183
459
  process.exit(1);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "agent-security-scanner-mcp",
3
- "version": "3.7.0",
3
+ "version": "3.9.0",
4
4
  "mcpName": "io.github.sinewaveai/agent-security-scanner-mcp",
5
- "description": "Security scanner MCP server for AI coding agents. Prompt injection firewall, package hallucination detection (4.3M+ packages), 1000+ vulnerability rules with AST & taint analysis, auto-fix. For Claude Code, Cursor, Windsurf, Cline.",
5
+ "description": "Security scanner MCP server for AI coding agents. Prompt injection firewall, package hallucination detection (4.3M+ packages), 1000+ vulnerability rules with AST & taint analysis, auto-fix. For Claude Code, Cursor, Windsurf, Cline, OpenClaw.",
6
6
  "main": "index.js",
7
7
  "type": "module",
8
8
  "bin": {
@@ -10,10 +10,11 @@
10
10
  },
11
11
  "scripts": {
12
12
  "start": "node index.js",
13
+ "postinstall": "node scripts/postinstall.js",
13
14
  "test": "vitest run",
14
15
  "test:watch": "vitest",
15
16
  "test:coverage": "vitest run --coverage",
16
- "test:redteam": "npx promptfoo eval --config tests/promptfoo/promptfooconfig.yaml"
17
+ "benchmark": "python3 benchmarks/benchmark_runner.py --save --compare-latest"
17
18
  },
18
19
  "keywords": [
19
20
  "mcp",
@@ -53,7 +54,9 @@
53
54
  "zed",
54
55
  "prompt-firewall",
55
56
  "auto-fix",
56
- "hallucination"
57
+ "hallucination",
58
+ "openclaw",
59
+ "clawdbot"
57
60
  ],
58
61
  "author": "Sinewave AI <divya@sinewave.ai>",
59
62
  "license": "MIT",
@@ -81,6 +84,13 @@
81
84
  "src/cli/*.js",
82
85
  "src/fix-patterns.js",
83
86
  "src/utils.js",
87
+ "src/daemon-client.js",
88
+ "src/dedup.js",
89
+ "src/context.js",
90
+ "src/config.js",
91
+ "src/history.js",
92
+ "src/typosquat.js",
93
+ "templates/**",
84
94
  "analyzer.py",
85
95
  "ast_parser.py",
86
96
  "generic_ast.py",
@@ -90,7 +100,11 @@
90
100
  "taint_analyzer.py",
91
101
  "requirements.txt",
92
102
  "rules/**",
93
- "packages/**"
103
+ "packages/**",
104
+ "skills/**",
105
+ "scripts/postinstall.js",
106
+ "cross_file_analyzer.py",
107
+ "daemon.py"
94
108
  ],
95
109
  "devDependencies": {
96
110
  "all-the-package-names": "^2.0.2349",