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/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\n');
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\n');
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)\n');
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\n');
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.0",
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",
@@ -430,6 +430,7 @@ class Finding:
430
430
  end_column: int = 0
431
431
  metavariables: Dict[str, str] = field(default_factory=dict)
432
432
  metadata: Dict[str, Any] = field(default_factory=dict)
433
+ confidence: str = "MEDIUM"
433
434
 
434
435
 
435
436
  class RuleEngine:
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 = "warning") -> Dict:
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
+ }