agent-security-scanner-mcp 3.3.0 → 3.4.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 +224 -2
- package/analyzer.py +22 -5
- package/cross_file_analyzer.py +216 -0
- package/index.js +104 -4
- package/package.json +10 -3
- package/pattern_matcher.py +1 -0
- package/regex_fallback.py +199 -1
- package/scripts/postinstall.js +25 -0
- package/src/cli/init-hooks.js +164 -0
- package/src/config.js +181 -0
- package/src/context.js +228 -0
- package/src/dedup.js +129 -0
- package/src/fix-patterns.js +66 -17
- package/src/tools/fix-security.js +31 -4
- package/src/tools/scan-diff.js +151 -0
- package/src/tools/scan-project.js +308 -0
- package/src/tools/scan-security.js +33 -5
- package/src/utils.js +76 -7
package/index.js
CHANGED
|
@@ -17,9 +17,12 @@ 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';
|
|
20
22
|
import { runInit } from './src/cli/init.js';
|
|
21
23
|
import { runDoctor } from './src/cli/doctor.js';
|
|
22
24
|
import { runDemo } from './src/cli/demo.js';
|
|
25
|
+
import { runInitHooks } from './src/cli/init-hooks.js';
|
|
23
26
|
|
|
24
27
|
// Handle both ESM and CJS bundling (Smithery bundles to CJS)
|
|
25
28
|
let __dirname;
|
|
@@ -134,6 +137,22 @@ server.tool(
|
|
|
134
137
|
scanAgentPrompt
|
|
135
138
|
);
|
|
136
139
|
|
|
140
|
+
// Register scan_git_diff tool
|
|
141
|
+
server.tool(
|
|
142
|
+
"scan_git_diff",
|
|
143
|
+
"Scan git diff for new security vulnerabilities. Only reports issues on changed lines. Use for PR reviews.",
|
|
144
|
+
scanDiffSchema,
|
|
145
|
+
scanDiff
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Register scan_project tool
|
|
149
|
+
server.tool(
|
|
150
|
+
"scan_project",
|
|
151
|
+
"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.",
|
|
152
|
+
scanProjectSchema,
|
|
153
|
+
scanProject
|
|
154
|
+
);
|
|
155
|
+
|
|
137
156
|
// ===========================================
|
|
138
157
|
// CLI COMMANDS - Extracted to src/cli/
|
|
139
158
|
// ===========================================
|
|
@@ -156,6 +175,11 @@ if (cliArgs[0] === 'init') {
|
|
|
156
175
|
console.error(` Error: ${err.message}\n`);
|
|
157
176
|
process.exit(1);
|
|
158
177
|
});
|
|
178
|
+
} else if (cliArgs[0] === 'init-hooks') {
|
|
179
|
+
runInitHooks(cliArgs.slice(1)).then(() => process.exit(0)).catch((err) => {
|
|
180
|
+
console.error(` Error: ${err.message}\n`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
});
|
|
159
183
|
} else if (cliArgs[0] === 'scan-prompt') {
|
|
160
184
|
// CLI mode: scan-prompt <text> [--verbosity minimal|compact|full]
|
|
161
185
|
const text = cliArgs[1];
|
|
@@ -236,26 +260,102 @@ if (cliArgs[0] === 'init') {
|
|
|
236
260
|
console.error(JSON.stringify({ error: err.message }));
|
|
237
261
|
process.exit(1);
|
|
238
262
|
});
|
|
263
|
+
} else if (cliArgs[0] === 'scan-project') {
|
|
264
|
+
// CLI mode: scan-project <dir> [--recursive] [--diff-only] [--cross-file] [--include '*.py'] [--exclude '*.test.js'] [--verbosity minimal|compact|full]
|
|
265
|
+
const dirPath = cliArgs[1];
|
|
266
|
+
if (!dirPath || dirPath.startsWith('--')) {
|
|
267
|
+
console.error('Usage: agent-security-scanner-mcp scan-project <directory> [--recursive] [--diff-only] [--cross-file] [--include <pattern>] [--exclude <pattern>] [--verbosity minimal|compact|full]');
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
271
|
+
const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
|
|
272
|
+
const recursive = !cliArgs.includes('--no-recursive');
|
|
273
|
+
const diffOnly = cliArgs.includes('--diff-only');
|
|
274
|
+
const crossFile = cliArgs.includes('--cross-file');
|
|
275
|
+
const includeIdx = cliArgs.indexOf('--include');
|
|
276
|
+
const includePatterns = includeIdx !== -1 ? [cliArgs[includeIdx + 1]] : undefined;
|
|
277
|
+
const excludeIdx = cliArgs.indexOf('--exclude');
|
|
278
|
+
const excludePatterns = excludeIdx !== -1 ? [cliArgs[excludeIdx + 1]] : undefined;
|
|
279
|
+
|
|
280
|
+
scanProject({ directory_path: dirPath, recursive, diff_only: diffOnly, cross_file: crossFile, include_patterns: includePatterns, exclude_patterns: excludePatterns, verbosity }).then(result => {
|
|
281
|
+
const output = JSON.parse(result.content[0].text);
|
|
282
|
+
console.log(JSON.stringify(output, null, 2));
|
|
283
|
+
const total = output.issues_count || output.total || 0;
|
|
284
|
+
process.exit(total > 0 ? 1 : 0);
|
|
285
|
+
}).catch(err => {
|
|
286
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
287
|
+
process.exit(1);
|
|
288
|
+
});
|
|
289
|
+
} else if (cliArgs[0] === 'scan-diff') {
|
|
290
|
+
// CLI mode: scan-diff [base] [target] [--verbosity minimal|compact|full]
|
|
291
|
+
// Parse positional args, skipping flag values
|
|
292
|
+
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
293
|
+
const flagValueIndices = new Set(verbosityIdx !== -1 ? [verbosityIdx, verbosityIdx + 1] : []);
|
|
294
|
+
const positionalArgs = cliArgs.slice(1).filter((arg, idx) => !arg.startsWith('--') && !flagValueIndices.has(idx + 1));
|
|
295
|
+
const baseRef = positionalArgs[0];
|
|
296
|
+
const targetRef = positionalArgs[1];
|
|
297
|
+
const verbosity = verbosityIdx !== -1 ? cliArgs[verbosityIdx + 1] : 'compact';
|
|
298
|
+
|
|
299
|
+
scanDiff({ base_ref: baseRef, target_ref: targetRef, verbosity }).then(result => {
|
|
300
|
+
const output = JSON.parse(result.content[0].text);
|
|
301
|
+
console.log(JSON.stringify(output, null, 2));
|
|
302
|
+
process.exit(output.issues_count > 0 || output.total > 0 ? 1 : 0);
|
|
303
|
+
}).catch(err => {
|
|
304
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
305
|
+
process.exit(1);
|
|
306
|
+
});
|
|
307
|
+
} else if (cliArgs[0] === 'benchmark') {
|
|
308
|
+
// CLI mode: benchmark [--save] [--json-only] [--compare-latest] [--corpus <path>]
|
|
309
|
+
const benchmarkPath = join(__dirname, 'benchmarks', 'benchmark_runner.py');
|
|
310
|
+
const benchArgs = [benchmarkPath];
|
|
311
|
+
|
|
312
|
+
// Pass through supported flags
|
|
313
|
+
for (let i = 1; i < cliArgs.length; i++) {
|
|
314
|
+
if (['--save', '--json-only', '--compare-latest'].includes(cliArgs[i])) {
|
|
315
|
+
benchArgs.push(cliArgs[i]);
|
|
316
|
+
} else if (cliArgs[i] === '--corpus' && cliArgs[i + 1]) {
|
|
317
|
+
benchArgs.push('--corpus', cliArgs[i + 1]);
|
|
318
|
+
i++;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
execFileSync('python3', benchArgs, { stdio: 'inherit', timeout: 300000 });
|
|
324
|
+
} catch (err) {
|
|
325
|
+
if (err.status) process.exit(err.status);
|
|
326
|
+
console.error(`Benchmark error: ${err.message}`);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
process.exit(0);
|
|
239
330
|
} else if (cliArgs[0] === '--help' || cliArgs[0] === '-h' || cliArgs[0] === 'help') {
|
|
240
331
|
console.log('\n agent-security-scanner-mcp\n');
|
|
241
332
|
console.log(' Commands:');
|
|
242
333
|
console.log(' init [client] Set up MCP config for a client');
|
|
334
|
+
console.log(' init-hooks Install Claude Code hooks for auto-scanning');
|
|
243
335
|
console.log(' doctor [--fix] Check environment & client configs');
|
|
244
|
-
console.log(' demo [--lang js] Generate vulnerable file + scan it
|
|
336
|
+
console.log(' demo [--lang js] Generate vulnerable file + scan it');
|
|
337
|
+
console.log(' benchmark [flags] Run accuracy benchmarks\n');
|
|
245
338
|
console.log(' CLI Tools (for scripts & OpenClaw):');
|
|
246
339
|
console.log(' scan-prompt <text> Scan prompt for injection attacks');
|
|
247
340
|
console.log(' scan-security <file> Scan file for vulnerabilities');
|
|
248
341
|
console.log(' check-package <n> <e> Check if package exists in ecosystem');
|
|
249
|
-
console.log(' scan-packages <f> <e> Scan file imports for hallucinated packages
|
|
342
|
+
console.log(' scan-packages <f> <e> Scan file imports for hallucinated packages');
|
|
343
|
+
console.log(' scan-project <dir> Scan directory for vulnerabilities with grading');
|
|
344
|
+
console.log(' scan-diff [base] [target] Scan git diff for new vulnerabilities\n');
|
|
250
345
|
console.log(' (no args) Start MCP server on stdio\n');
|
|
251
346
|
console.log(' Options:');
|
|
252
347
|
console.log(' --verbosity <level> minimal|compact|full (default: compact)');
|
|
253
|
-
console.log(' --format <type> json|sarif (scan-security only)
|
|
348
|
+
console.log(' --format <type> json|sarif (scan-security only)');
|
|
349
|
+
console.log(' --include <pattern> Include only matching files (scan-project)');
|
|
350
|
+
console.log(' --exclude <pattern> Exclude matching files (scan-project)\n');
|
|
254
351
|
console.log(' Examples:');
|
|
255
352
|
console.log(' npx agent-security-scanner-mcp init');
|
|
256
353
|
console.log(' npx agent-security-scanner-mcp scan-prompt "ignore previous instructions"');
|
|
257
354
|
console.log(' npx agent-security-scanner-mcp scan-security ./app.py --verbosity minimal');
|
|
258
|
-
console.log(' npx agent-security-scanner-mcp check-package flask pypi
|
|
355
|
+
console.log(' npx agent-security-scanner-mcp check-package flask pypi');
|
|
356
|
+
console.log(' npx agent-security-scanner-mcp scan-project ./src --verbosity minimal');
|
|
357
|
+
console.log(' npx agent-security-scanner-mcp scan-diff HEAD~1');
|
|
358
|
+
console.log(' npx agent-security-scanner-mcp benchmark --save --compare-latest\n');
|
|
259
359
|
process.exit(0);
|
|
260
360
|
} else {
|
|
261
361
|
// Normal MCP server mode
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-security-scanner-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"mcpName": "io.github.sinewaveai/agent-security-scanner-mcp",
|
|
5
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",
|
|
@@ -10,9 +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
|
-
"test:coverage": "vitest run --coverage"
|
|
16
|
+
"test:coverage": "vitest run --coverage",
|
|
17
|
+
"benchmark": "python3 benchmarks/benchmark_runner.py --save --compare-latest"
|
|
16
18
|
},
|
|
17
19
|
"keywords": [
|
|
18
20
|
"mcp",
|
|
@@ -82,6 +84,9 @@
|
|
|
82
84
|
"src/cli/*.js",
|
|
83
85
|
"src/fix-patterns.js",
|
|
84
86
|
"src/utils.js",
|
|
87
|
+
"src/dedup.js",
|
|
88
|
+
"src/context.js",
|
|
89
|
+
"src/config.js",
|
|
85
90
|
"analyzer.py",
|
|
86
91
|
"ast_parser.py",
|
|
87
92
|
"generic_ast.py",
|
|
@@ -92,7 +97,9 @@
|
|
|
92
97
|
"requirements.txt",
|
|
93
98
|
"rules/**",
|
|
94
99
|
"packages/**",
|
|
95
|
-
"skills/**"
|
|
100
|
+
"skills/**",
|
|
101
|
+
"scripts/postinstall.js",
|
|
102
|
+
"cross_file_analyzer.py"
|
|
96
103
|
],
|
|
97
104
|
"devDependencies": {
|
|
98
105
|
"all-the-package-names": "^2.0.2349",
|
package/pattern_matcher.py
CHANGED
package/regex_fallback.py
CHANGED
|
@@ -9,10 +9,207 @@ from typing import List, Dict, Optional
|
|
|
9
9
|
import re
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
# Severity classification by vulnerability class
|
|
13
|
+
SEVERITY_MAP = {
|
|
14
|
+
# ERROR - exploitable vulnerabilities (injection, RCE, deserialization)
|
|
15
|
+
'sql-injection': 'error',
|
|
16
|
+
'sql-injection-query': 'error',
|
|
17
|
+
'sql-injection-sprintf': 'error',
|
|
18
|
+
'sql-injection-where': 'error',
|
|
19
|
+
'sql-injection-order': 'error',
|
|
20
|
+
'sql-injection-raw': 'error',
|
|
21
|
+
'sql-injection-db-cursor': 'error',
|
|
22
|
+
'sql-injection-using-sqlalchemy': 'error',
|
|
23
|
+
'sql-injection-sqlcommand': 'error',
|
|
24
|
+
'sql-injection-sqlquery': 'error',
|
|
25
|
+
'sql-injection-concat': 'error',
|
|
26
|
+
'command-injection': 'error',
|
|
27
|
+
'command-injection-exec': 'error',
|
|
28
|
+
'command-injection-system': 'error',
|
|
29
|
+
'command-injection-open': 'error',
|
|
30
|
+
'command-injection-process-start': 'error',
|
|
31
|
+
'child-process-exec': 'error',
|
|
32
|
+
'spawn-shell': 'error',
|
|
33
|
+
'dangerous-subprocess-use': 'error',
|
|
34
|
+
'dangerous-system-call': 'error',
|
|
35
|
+
'eval-detected': 'error',
|
|
36
|
+
'eval-usage': 'error',
|
|
37
|
+
'exec-detected': 'error',
|
|
38
|
+
'pickle-load': 'error',
|
|
39
|
+
'unsafe-unserialize': 'error',
|
|
40
|
+
'unsafe-yaml-load': 'error',
|
|
41
|
+
'unsafe-marshal': 'error',
|
|
42
|
+
'yaml-load': 'error',
|
|
43
|
+
'file-inclusion': 'error',
|
|
44
|
+
'path-traversal': 'error',
|
|
45
|
+
'xss-echo': 'error',
|
|
46
|
+
'xss-raw': 'error',
|
|
47
|
+
'ssrf': 'error',
|
|
48
|
+
'open-redirect': 'error',
|
|
49
|
+
'backticks-exec': 'error',
|
|
50
|
+
'preg-code-exec': 'error',
|
|
51
|
+
'assert-usage': 'error',
|
|
52
|
+
'insecure-deserialization-binaryformatter': 'error',
|
|
53
|
+
'insecure-deserialization-xmlserializer': 'error',
|
|
54
|
+
'libc-system-call': 'error',
|
|
55
|
+
'format-string-printf': 'error',
|
|
56
|
+
'format-string-syslog': 'error',
|
|
57
|
+
'xss-innerhtml': 'error',
|
|
58
|
+
'xss-response-write': 'error',
|
|
59
|
+
'path-traversal-directory-delete': 'error',
|
|
60
|
+
'path-traversal-file-delete': 'error',
|
|
61
|
+
'path-traversal-file-read': 'error',
|
|
62
|
+
|
|
63
|
+
# WARNING - risky patterns requiring attention
|
|
64
|
+
'innerHTML': 'warning',
|
|
65
|
+
'outerHTML': 'warning',
|
|
66
|
+
'document-write': 'warning',
|
|
67
|
+
'insertAdjacentHTML': 'warning',
|
|
68
|
+
'dangerouslySetInnerHTML': 'warning',
|
|
69
|
+
'function-constructor': 'warning',
|
|
70
|
+
'setTimeout-string': 'warning',
|
|
71
|
+
'strcpy-usage': 'warning',
|
|
72
|
+
'strcat-usage': 'warning',
|
|
73
|
+
'sprintf-usage': 'warning',
|
|
74
|
+
'vsprintf-usage': 'warning',
|
|
75
|
+
'gets-usage': 'warning',
|
|
76
|
+
'system-usage': 'warning',
|
|
77
|
+
'popen-usage': 'warning',
|
|
78
|
+
'hardcoded-password': 'warning',
|
|
79
|
+
'hardcoded-secret': 'warning',
|
|
80
|
+
'hardcoded-api-key': 'warning',
|
|
81
|
+
'hardcoded-connection-string': 'warning',
|
|
82
|
+
'session-secret-hardcoded': 'warning',
|
|
83
|
+
'ssl-verify-disabled': 'warning',
|
|
84
|
+
'curl-ssl-disabled': 'warning',
|
|
85
|
+
'csrf-disabled': 'warning',
|
|
86
|
+
'mass-assignment-permit-all': 'warning',
|
|
87
|
+
'constantize': 'warning',
|
|
88
|
+
'render-inline': 'warning',
|
|
89
|
+
'privileged-container': 'warning',
|
|
90
|
+
'run-as-root': 'warning',
|
|
91
|
+
'allow-privilege-escalation': 'warning',
|
|
92
|
+
'host-network': 'warning',
|
|
93
|
+
'host-pid': 'warning',
|
|
94
|
+
'host-path': 'warning',
|
|
95
|
+
'secrets-in-env': 'warning',
|
|
96
|
+
'cluster-admin-binding': 'warning',
|
|
97
|
+
'capabilities-add': 'warning',
|
|
98
|
+
'no-readonly-root': 'warning',
|
|
99
|
+
'wildcard-rbac': 'warning',
|
|
100
|
+
's3-public-read': 'warning',
|
|
101
|
+
'security-group-open-ingress': 'warning',
|
|
102
|
+
'rds-public-access': 'warning',
|
|
103
|
+
'rds-encryption-disabled': 'warning',
|
|
104
|
+
'rds-deletion-protection': 'warning',
|
|
105
|
+
'cloudtrail-disabled': 'warning',
|
|
106
|
+
'kms-key-rotation': 'warning',
|
|
107
|
+
'ebs-encryption-disabled': 'warning',
|
|
108
|
+
'ec2-imdsv1': 'warning',
|
|
109
|
+
'phpinfo-exposure': 'warning',
|
|
110
|
+
'error-display': 'warning',
|
|
111
|
+
'permissive-cors': 'warning',
|
|
112
|
+
'mcrypt-deprecated': 'warning',
|
|
113
|
+
'aws-access-key-id': 'warning',
|
|
114
|
+
'aws-secret-access-key': 'warning',
|
|
115
|
+
'github-pat': 'warning',
|
|
116
|
+
'stripe-api-key': 'warning',
|
|
117
|
+
'private-key-rsa': 'warning',
|
|
118
|
+
'database-url': 'warning',
|
|
119
|
+
'jwt-token': 'warning',
|
|
120
|
+
'openai-api-key': 'warning',
|
|
121
|
+
'python.lang.security.audit.hardcoded-password': 'warning',
|
|
122
|
+
'python.lang.security.audit.hardcoded-api-key': 'warning',
|
|
123
|
+
'generic.secrets.security.hardcoded-password': 'warning',
|
|
124
|
+
'generic.secrets.security.hardcoded-api-key': 'warning',
|
|
125
|
+
|
|
126
|
+
# INFO - informational / hygiene / low-risk patterns
|
|
127
|
+
'weak-hash-md5': 'info',
|
|
128
|
+
'weak-hash-sha1': 'info',
|
|
129
|
+
'weak-hash': 'info',
|
|
130
|
+
'weak-cipher': 'info',
|
|
131
|
+
'weak-cipher-des': 'info',
|
|
132
|
+
'ecb-mode': 'info',
|
|
133
|
+
'weak-random': 'info',
|
|
134
|
+
'insecure-random': 'info',
|
|
135
|
+
'insecure-hash-md5': 'info',
|
|
136
|
+
'insecure-hash-sha1': 'info',
|
|
137
|
+
'insecure-memset': 'info',
|
|
138
|
+
'strtok-usage': 'info',
|
|
139
|
+
'insecure-tempfile': 'info',
|
|
140
|
+
'unchecked-return': 'info',
|
|
141
|
+
'unsafe-block': 'info',
|
|
142
|
+
'unwrap-usage': 'info',
|
|
143
|
+
'raw-pointer-deref': 'info',
|
|
144
|
+
'panic-usage': 'info',
|
|
145
|
+
'compile-detected': 'info',
|
|
146
|
+
'scanf-usage': 'info',
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Confidence classification by rule ID
|
|
150
|
+
CONFIDENCE_MAP = {
|
|
151
|
+
# HIGH - very specific patterns, low false-positive rate
|
|
152
|
+
'sql-injection-db-cursor': 'HIGH',
|
|
153
|
+
'sql-injection-using-sqlalchemy': 'HIGH',
|
|
154
|
+
'sql-injection-sqlcommand': 'HIGH',
|
|
155
|
+
'sql-injection-sqlquery': 'HIGH',
|
|
156
|
+
'sql-injection-concat': 'HIGH',
|
|
157
|
+
'format-string-printf': 'HIGH',
|
|
158
|
+
'format-string-syslog': 'HIGH',
|
|
159
|
+
'pickle-load': 'HIGH',
|
|
160
|
+
'eval-detected': 'HIGH',
|
|
161
|
+
'eval-usage': 'HIGH',
|
|
162
|
+
'exec-detected': 'HIGH',
|
|
163
|
+
'child-process-exec': 'HIGH',
|
|
164
|
+
'dangerous-subprocess-use': 'HIGH',
|
|
165
|
+
'dangerous-system-call': 'HIGH',
|
|
166
|
+
'hardcoded-password': 'HIGH',
|
|
167
|
+
'hardcoded-connection-string': 'HIGH',
|
|
168
|
+
'aws-access-key-id': 'HIGH',
|
|
169
|
+
'github-pat': 'HIGH',
|
|
170
|
+
'stripe-api-key': 'HIGH',
|
|
171
|
+
'private-key-rsa': 'HIGH',
|
|
172
|
+
'openai-api-key': 'HIGH',
|
|
173
|
+
'unsafe-unserialize': 'HIGH',
|
|
174
|
+
'backticks-exec': 'HIGH',
|
|
175
|
+
'preg-code-exec': 'HIGH',
|
|
176
|
+
'file-inclusion': 'HIGH',
|
|
177
|
+
'gets-usage': 'HIGH',
|
|
178
|
+
'insecure-deserialization-binaryformatter': 'HIGH',
|
|
179
|
+
'insecure-deserialization-xmlserializer': 'HIGH',
|
|
180
|
+
|
|
181
|
+
# LOW - broad patterns with high false-positive rate
|
|
182
|
+
'compile-detected': 'LOW',
|
|
183
|
+
'unsafe-block': 'LOW',
|
|
184
|
+
'unwrap-usage': 'LOW',
|
|
185
|
+
'insecure-memset': 'LOW',
|
|
186
|
+
'strtok-usage': 'LOW',
|
|
187
|
+
'unchecked-return': 'LOW',
|
|
188
|
+
'scanf-usage': 'LOW',
|
|
189
|
+
'panic-usage': 'LOW',
|
|
190
|
+
'raw-pointer-deref': 'LOW',
|
|
191
|
+
'insecure-random': 'LOW',
|
|
192
|
+
'weak-random': 'LOW',
|
|
193
|
+
'insecure-hash-md5': 'LOW',
|
|
194
|
+
'insecure-hash-sha1': 'LOW',
|
|
195
|
+
'weak-hash-md5': 'LOW',
|
|
196
|
+
'weak-hash-sha1': 'LOW',
|
|
197
|
+
'weak-hash': 'LOW',
|
|
198
|
+
'database-url': 'LOW',
|
|
199
|
+
|
|
200
|
+
# Everything else defaults to MEDIUM
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
12
204
|
def _make_finding(rule_id: str, line_idx: int, line: str, col_start: int = 0, col_end: Optional[int] = None,
|
|
13
|
-
message: Optional[str] = None, severity: str =
|
|
205
|
+
message: Optional[str] = None, severity: Optional[str] = None,
|
|
206
|
+
confidence: Optional[str] = None) -> Dict:
|
|
14
207
|
if col_end is None:
|
|
15
208
|
col_end = max(col_start + 1, len(line.rstrip("\n")))
|
|
209
|
+
if severity is None:
|
|
210
|
+
severity = SEVERITY_MAP.get(rule_id, "warning")
|
|
211
|
+
if confidence is None:
|
|
212
|
+
confidence = CONFIDENCE_MAP.get(rule_id, "MEDIUM")
|
|
16
213
|
return {
|
|
17
214
|
"ruleId": rule_id,
|
|
18
215
|
"message": message or f"[Regex] {rule_id}",
|
|
@@ -22,6 +219,7 @@ def _make_finding(rule_id: str, line_idx: int, line: str, col_start: int = 0, co
|
|
|
22
219
|
"endColumn": col_end,
|
|
23
220
|
"length": max(0, col_end - col_start),
|
|
24
221
|
"severity": severity,
|
|
222
|
+
"confidence": confidence,
|
|
25
223
|
"metadata": {"source": "regex-fallback"},
|
|
26
224
|
"metavariables": {},
|
|
27
225
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postinstall.js - Attempt to install Python dependencies for tree-sitter AST engine.
|
|
4
|
+
* If installation fails, the scanner gracefully falls back to regex-only mode.
|
|
5
|
+
*/
|
|
6
|
+
import { execFileSync } from "child_process";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const requirementsPath = join(__dirname, "..", "requirements.txt");
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
execFileSync("python3", ["-m", "pip", "install", "-r", requirementsPath, "--user", "--quiet"], {
|
|
15
|
+
timeout: 120000,
|
|
16
|
+
stdio: "inherit",
|
|
17
|
+
});
|
|
18
|
+
console.log("[postinstall] Python dependencies installed - AST engine enabled.");
|
|
19
|
+
} catch {
|
|
20
|
+
console.log(
|
|
21
|
+
"[postinstall] Could not install Python dependencies (tree-sitter).\n" +
|
|
22
|
+
" The scanner will run in regex-only mode, which still catches common vulnerabilities.\n" +
|
|
23
|
+
" To enable AST analysis later, run: python3 -m pip install -r requirements.txt"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// src/cli/init-hooks.js
|
|
2
|
+
// CLI command: init-hooks
|
|
3
|
+
// Installs Claude Code hooks for automatic security scanning on file write/edit.
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
|
|
8
|
+
const SCANNER_HOOK_MARKER = 'agent-security-scanner-mcp';
|
|
9
|
+
|
|
10
|
+
function buildHooksConfig(withPromptGuard) {
|
|
11
|
+
const hooks = {
|
|
12
|
+
'post-tool-use': [
|
|
13
|
+
{
|
|
14
|
+
matcher: 'Write|Edit|MultiEdit',
|
|
15
|
+
command: `npx agent-security-scanner-mcp scan-security "$TOOL_INPUT_FILE_PATH" --verbosity minimal`,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (withPromptGuard) {
|
|
21
|
+
hooks['pre-tool-use'] = [
|
|
22
|
+
{
|
|
23
|
+
matcher: 'Bash',
|
|
24
|
+
command: `npx agent-security-scanner-mcp scan-prompt "$TOOL_INPUT_COMMAND" --verbosity minimal`,
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return hooks;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function backupTimestamp() {
|
|
33
|
+
const d = new Date();
|
|
34
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
35
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseFlags(args) {
|
|
39
|
+
const flags = { dryRun: false, path: null, withPromptGuard: false };
|
|
40
|
+
let i = 0;
|
|
41
|
+
while (i < args.length) {
|
|
42
|
+
const arg = args[i];
|
|
43
|
+
if (arg === '--dry-run') flags.dryRun = true;
|
|
44
|
+
else if (arg === '--path' && i + 1 < args.length) flags.path = args[++i];
|
|
45
|
+
else if (arg === '--with-prompt-guard') flags.withPromptGuard = true;
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
return flags;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function containsScannerHook(hooksObj) {
|
|
52
|
+
if (!hooksObj || typeof hooksObj !== 'object') return false;
|
|
53
|
+
for (const eventHooks of Object.values(hooksObj)) {
|
|
54
|
+
if (!Array.isArray(eventHooks)) continue;
|
|
55
|
+
if (eventHooks.some(h => h.command && h.command.includes(SCANNER_HOOK_MARKER))) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mergeHooks(existingHooks, newHooks) {
|
|
63
|
+
const merged = { ...existingHooks };
|
|
64
|
+
|
|
65
|
+
for (const [event, hooks] of Object.entries(newHooks)) {
|
|
66
|
+
if (!merged[event]) {
|
|
67
|
+
merged[event] = hooks;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Filter out existing scanner hooks for this event
|
|
72
|
+
const nonScanner = merged[event].filter(h =>
|
|
73
|
+
!h.command || !h.command.includes(SCANNER_HOOK_MARKER)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
merged[event] = [...nonScanner, ...hooks];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return merged;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function runInitHooks(args) {
|
|
83
|
+
const flags = parseFlags(args);
|
|
84
|
+
|
|
85
|
+
console.log('\n Agentic Security - Claude Code Hooks Setup\n');
|
|
86
|
+
|
|
87
|
+
const settingsDir = flags.path || join(process.cwd(), '.claude');
|
|
88
|
+
const settingsPath = join(settingsDir, 'settings.json');
|
|
89
|
+
|
|
90
|
+
console.log(` Settings: ${settingsPath}`);
|
|
91
|
+
console.log(` Prompt guard: ${flags.withPromptGuard ? 'enabled' : 'disabled (use --with-prompt-guard to enable)'}`);
|
|
92
|
+
console.log('');
|
|
93
|
+
|
|
94
|
+
const newHooks = buildHooksConfig(flags.withPromptGuard);
|
|
95
|
+
|
|
96
|
+
// Read existing settings
|
|
97
|
+
let existing = {};
|
|
98
|
+
let fileExisted = false;
|
|
99
|
+
if (existsSync(settingsPath)) {
|
|
100
|
+
fileExisted = true;
|
|
101
|
+
try {
|
|
102
|
+
existing = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.error(` ERROR: Invalid JSON in ${settingsPath}`);
|
|
105
|
+
console.error(` ${e.message}\n`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (containsScannerHook(existing.hooks)) {
|
|
111
|
+
console.log(' Scanner hooks already configured. Updating...');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Merge hooks non-destructively
|
|
115
|
+
const mergedHooks = mergeHooks(existing.hooks || {}, newHooks);
|
|
116
|
+
const merged = { ...existing, hooks: mergedHooks };
|
|
117
|
+
const output = JSON.stringify(merged, null, 2) + '\n';
|
|
118
|
+
|
|
119
|
+
if (flags.dryRun) {
|
|
120
|
+
console.log(' [dry-run] Would write:\n');
|
|
121
|
+
console.log(' ' + output.split('\n').join('\n '));
|
|
122
|
+
console.log(' No changes made.\n');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!existsSync(settingsDir)) {
|
|
127
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
128
|
+
console.log(` Created directory: ${settingsDir}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (fileExisted) {
|
|
132
|
+
const backupPath = `${settingsPath}.bak-${backupTimestamp()}`;
|
|
133
|
+
copyFileSync(settingsPath, backupPath);
|
|
134
|
+
console.log(` Backup: ${backupPath}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
writeFileSync(settingsPath, output);
|
|
138
|
+
console.log(` Wrote: ${settingsPath}\n`);
|
|
139
|
+
|
|
140
|
+
console.log(' Hooks installed:');
|
|
141
|
+
for (const [event, hooks] of Object.entries(newHooks)) {
|
|
142
|
+
for (const hook of hooks) {
|
|
143
|
+
console.log(` - [${event}] Matcher: ${hook.matcher}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log('\n Security scanning is now automatic for file writes and edits.');
|
|
148
|
+
console.log(' Restart Claude Code for hooks to take effect.\n');
|
|
149
|
+
|
|
150
|
+
if (!existsSync(join(process.cwd(), '.scannerrc.yaml')) &&
|
|
151
|
+
!existsSync(join(process.cwd(), '.scannerrc.yml')) &&
|
|
152
|
+
!existsSync(join(process.cwd(), '.scannerrc.json'))) {
|
|
153
|
+
console.log(' Tip: Create a .scannerrc.yaml to customize scanning:');
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log(' version: 1');
|
|
156
|
+
console.log(' suppress:');
|
|
157
|
+
console.log(' - rule: "insecure-random"');
|
|
158
|
+
console.log(' exclude:');
|
|
159
|
+
console.log(' - "node_modules/**"');
|
|
160
|
+
console.log(' - "dist/**"');
|
|
161
|
+
console.log(' severity_threshold: "warning"');
|
|
162
|
+
console.log('');
|
|
163
|
+
}
|
|
164
|
+
}
|