agent-security-scanner-mcp 3.1.0 → 3.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-security-scanner-mcp",
3
- "version": "3.1.0",
3
+ "version": "3.2.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.",
6
6
  "main": "index.js",
@@ -76,6 +76,10 @@
76
76
  "LICENSE",
77
77
  "server.json",
78
78
  "index.js",
79
+ "src/tools/*.js",
80
+ "src/cli/*.js",
81
+ "src/fix-patterns.js",
82
+ "src/utils.js",
79
83
  "analyzer.py",
80
84
  "ast_parser.py",
81
85
  "generic_ast.py",
@@ -0,0 +1,119 @@
1
+ import sys
2
+ import json
3
+ import re
4
+ import os
5
+
6
+ # Add the directory containing this script to the path
7
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
8
+
9
+ from rules import get_rules, get_rules_for_language, get_rule_stats
10
+
11
+ # File extension to language mapping
12
+ EXTENSION_MAP = {
13
+ '.py': 'python',
14
+ '.js': 'javascript',
15
+ '.ts': 'typescript',
16
+ '.tsx': 'typescript',
17
+ '.jsx': 'javascript',
18
+ '.java': 'java',
19
+ '.go': 'go',
20
+ '.rb': 'ruby',
21
+ '.php': 'php',
22
+ '.cs': 'csharp',
23
+ '.rs': 'rust',
24
+ '.c': 'c',
25
+ '.cpp': 'cpp',
26
+ '.h': 'c',
27
+ '.hpp': 'cpp',
28
+ '.sql': 'sql',
29
+ '.dockerfile': 'dockerfile',
30
+ '.yaml': 'yaml',
31
+ '.yml': 'yaml',
32
+ '.json': 'json',
33
+ '.tf': 'terraform',
34
+ '.hcl': 'terraform',
35
+ }
36
+
37
+ def detect_language(file_path):
38
+ """Detect the programming language from file extension or name"""
39
+ basename = os.path.basename(file_path).lower()
40
+
41
+ if basename == 'dockerfile' or basename.startswith('dockerfile.') or basename.startswith('dockerfile_'):
42
+ return 'dockerfile'
43
+
44
+ _, ext = os.path.splitext(file_path.lower())
45
+ return EXTENSION_MAP.get(ext, 'generic')
46
+
47
+ def analyze_file(file_path):
48
+ """Analyze a single file for security vulnerabilities"""
49
+ issues = []
50
+
51
+ try:
52
+ language = detect_language(file_path)
53
+ rules = get_rules_for_language(language)
54
+
55
+ with open(file_path, 'r', encoding='utf-8') as f:
56
+ lines = f.readlines()
57
+ content = ''.join(lines)
58
+
59
+ for line_index, line in enumerate(lines):
60
+ original_line = line
61
+ line = line.strip()
62
+ if not line:
63
+ continue
64
+
65
+ # Skip comment-only lines (basic detection)
66
+ if line.startswith('#') or line.startswith('//') or line.startswith('*'):
67
+ continue
68
+
69
+ for rule_id, rule in rules.items():
70
+ for pattern in rule['patterns']:
71
+ try:
72
+ # Use IGNORECASE for better detection (API_KEY vs api_key)
73
+ matches = re.finditer(pattern, line, re.IGNORECASE)
74
+ for match in matches:
75
+ # Calculate column based on original line (preserve indentation)
76
+ col_offset = len(original_line) - len(original_line.lstrip())
77
+ issues.append({
78
+ 'ruleId': rule['id'],
79
+ 'message': f"[{rule['name']}] {rule['message']}",
80
+ 'line': line_index,
81
+ 'column': match.start() + col_offset,
82
+ 'length': match.end() - match.start(),
83
+ 'severity': rule['severity'],
84
+ 'metadata': rule.get('metadata', {})
85
+ })
86
+ except re.error:
87
+ # Skip invalid regex patterns
88
+ continue
89
+
90
+ except Exception as e:
91
+ return {'error': str(e)}
92
+
93
+ # Deduplicate issues (same rule, same line)
94
+ seen = set()
95
+ unique_issues = []
96
+ for issue in issues:
97
+ key = (issue['ruleId'], issue['line'], issue['column'])
98
+ if key not in seen:
99
+ seen.add(key)
100
+ unique_issues.append(issue)
101
+
102
+ return unique_issues
103
+
104
+ def main():
105
+ if len(sys.argv) < 2:
106
+ print(json.dumps({'error': 'No file path provided'}))
107
+ sys.exit(1)
108
+
109
+ file_path = sys.argv[1]
110
+
111
+ if not os.path.exists(file_path):
112
+ print(json.dumps({'error': f'File not found: {file_path}'}))
113
+ sys.exit(1)
114
+
115
+ results = analyze_file(file_path)
116
+ print(json.dumps(results))
117
+
118
+ if __name__ == '__main__':
119
+ main()
@@ -0,0 +1,238 @@
1
+ import { execFileSync } from "child_process";
2
+ import { writeFileSync, unlinkSync } from "fs";
3
+ import { join } from "path";
4
+ import { createInterface } from "readline";
5
+ import { dirname } from "path";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // Handle both ESM and CJS bundling (Smithery bundles to CJS)
9
+ let __dirname;
10
+ try {
11
+ __dirname = dirname(fileURLToPath(import.meta.url));
12
+ } catch {
13
+ __dirname = process.cwd();
14
+ }
15
+
16
+ const DEMO_TEMPLATES = {
17
+ js: {
18
+ ext: 'js',
19
+ name: 'JavaScript',
20
+ code: `const express = require("express");
21
+ const child_process = require("child_process");
22
+ const app = express();
23
+
24
+ // SQL Injection vulnerability
25
+ app.get("/user", (req, res) => {
26
+ const userId = req.query.id;
27
+ db.query("SELECT * FROM users WHERE id = " + userId, (err, result) => {
28
+ res.send(result);
29
+ });
30
+ });
31
+
32
+ // XSS vulnerability
33
+ app.get("/profile", (req, res) => {
34
+ const name = req.query.name;
35
+ document.getElementById("welcome").innerHTML = name;
36
+ });
37
+
38
+ // Command Injection vulnerability
39
+ app.get("/run", (req, res) => {
40
+ const cmd = req.query.cmd;
41
+ child_process.exec("ls " + cmd, (err, stdout) => {
42
+ res.send(stdout);
43
+ });
44
+ });
45
+ `
46
+ },
47
+ py: {
48
+ ext: 'py',
49
+ name: 'Python',
50
+ code: `import pickle
51
+ import subprocess
52
+ import hashlib
53
+
54
+ API_SECRET = "stripe_test_FAKEFAKEFAKEFAKE1234"
55
+
56
+ def get_user(user_id):
57
+ query = f"SELECT * FROM users WHERE id = {user_id}"
58
+ cursor.execute(query)
59
+ return cursor.fetchone()
60
+
61
+ def load_data(data):
62
+ return pickle.loads(data)
63
+
64
+ def run_command(cmd):
65
+ return subprocess.call(cmd, shell=True)
66
+
67
+ def hash_password(password):
68
+ return hashlib.md5(password.encode()).hexdigest()
69
+ `
70
+ },
71
+ go: {
72
+ ext: 'go',
73
+ name: 'Go',
74
+ code: `package main
75
+
76
+ import (
77
+ \t"crypto/md5"
78
+ \t"database/sql"
79
+ \t"fmt"
80
+ \t"net/http"
81
+ \t"os/exec"
82
+ )
83
+
84
+ var dbPassword = "super_secret_password_123"
85
+
86
+ func getUser(w http.ResponseWriter, r *http.Request) {
87
+ \tid := r.URL.Query().Get("id")
88
+ \tquery := fmt.Sprintf("SELECT * FROM users WHERE id = %s", id)
89
+ \tdb.Query(query)
90
+ }
91
+
92
+ func runCmd(w http.ResponseWriter, r *http.Request) {
93
+ \tcmd := r.URL.Query().Get("cmd")
94
+ \tout, _ := exec.Command("sh", "-c", cmd).Output()
95
+ \tw.Write(out)
96
+ }
97
+
98
+ func hashData(data string) string {
99
+ \th := md5.Sum([]byte(data))
100
+ \treturn fmt.Sprintf("%x", h)
101
+ }
102
+ `
103
+ },
104
+ java: {
105
+ ext: 'java',
106
+ name: 'Java',
107
+ code: `import java.sql.*;
108
+ import java.io.*;
109
+ import java.security.MessageDigest;
110
+
111
+ public class VulnDemo {
112
+ private static final String DB_PASSWORD = "admin123";
113
+
114
+ public ResultSet getUser(String userId) throws SQLException {
115
+ Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/db");
116
+ Statement stmt = conn.createStatement();
117
+ return stmt.executeQuery("SELECT * FROM users WHERE id = " + userId);
118
+ }
119
+
120
+ public String runCommand(String cmd) throws IOException {
121
+ Runtime rt = Runtime.getRuntime();
122
+ Process proc = rt.exec(cmd);
123
+ BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
124
+ return reader.readLine();
125
+ }
126
+
127
+ public String hashPassword(String password) throws Exception {
128
+ MessageDigest md = MessageDigest.getInstance("MD5");
129
+ byte[] hash = md.digest(password.getBytes());
130
+ return new String(hash);
131
+ }
132
+ }
133
+ `
134
+ }
135
+ };
136
+
137
+ function parseDemoFlags(args) {
138
+ const flags = { lang: 'js' };
139
+ let i = 0;
140
+ while (i < args.length) {
141
+ const arg = args[i];
142
+ if ((arg === '--lang' || arg === '-l') && i + 1 < args.length) {
143
+ flags.lang = args[++i].toLowerCase();
144
+ } else if (!arg.startsWith('-')) {
145
+ flags.lang = arg.toLowerCase();
146
+ }
147
+ i++;
148
+ }
149
+ return flags;
150
+ }
151
+
152
+ function checkCommand(cmd, args) {
153
+ try {
154
+ const out = execFileSync(cmd, args, { timeout: 10000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
155
+ return { ok: true, output: out.trim() };
156
+ } catch {
157
+ return { ok: false, output: null };
158
+ }
159
+ }
160
+
161
+ export async function runDemo(args) {
162
+ const flags = parseDemoFlags(args);
163
+ const template = DEMO_TEMPLATES[flags.lang];
164
+ if (!template) {
165
+ console.log(`\n Unknown language: "${flags.lang}"`);
166
+ console.log(` Available: ${Object.keys(DEMO_TEMPLATES).join(', ')}\n`);
167
+ process.exit(1);
168
+ }
169
+
170
+ const filename = `vuln-demo.${template.ext}`;
171
+ const filepath = join(process.cwd(), filename);
172
+
173
+ console.log(`\n agent-security-scanner-mcp demo\n`);
174
+ console.log(` Creating ${filename} with 3 intentional vulnerabilities...\n`);
175
+
176
+ // Write the vulnerable file
177
+ writeFileSync(filepath, template.code);
178
+
179
+ // Run the analyzer
180
+ const analyzerPath = join(__dirname, '..', '..', 'analyzer.py');
181
+ let pythonCmd = 'python3';
182
+ const py3 = checkCommand('python3', ['--version']);
183
+ if (!py3.ok) {
184
+ const py = checkCommand('python', ['--version']);
185
+ if (py.ok && py.output.includes('3.')) {
186
+ pythonCmd = 'python';
187
+ } else {
188
+ console.log(` Error: Python 3 not found. Run "npx agent-security-scanner-mcp doctor" to diagnose.\n`);
189
+ unlinkSync(filepath);
190
+ process.exit(1);
191
+ }
192
+ }
193
+
194
+ let results;
195
+ try {
196
+ const output = execFileSync(pythonCmd, [analyzerPath, filepath], { timeout: 30000, encoding: 'utf-8' });
197
+ results = JSON.parse(output);
198
+ } catch (e) {
199
+ console.log(` Error running analyzer: ${e.message}\n`);
200
+ unlinkSync(filepath);
201
+ process.exit(1);
202
+ }
203
+
204
+ // Display results
205
+ console.log(` Scanning...\n`);
206
+
207
+ if (results.length === 0) {
208
+ console.log(` No issues found (unexpected for demo file).\n`);
209
+ } else {
210
+ console.log(` Found ${results.length} issue(s):\n`);
211
+ for (const issue of results) {
212
+ const severity = (issue.severity || 'error').toUpperCase();
213
+ const icon = severity === 'ERROR' ? '\u2717' : severity === 'WARNING' ? '\u2717' : '\u2022';
214
+ console.log(` ${icon} ${severity.padEnd(8)} Line ${String(issue.line).padEnd(4)} ${issue.message}`);
215
+ if (issue.metadata) {
216
+ const refs = [issue.metadata.cwe, issue.metadata.owasp].filter(Boolean).join(' | ');
217
+ if (refs) console.log(` ${refs}`);
218
+ }
219
+ }
220
+ console.log(`\n ${results.length} vulnerabilities detected.\n`);
221
+ }
222
+
223
+ // Ask to keep or delete
224
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
225
+ const answer = await new Promise((resolve) => {
226
+ rl.question(` Keep ${filename} for testing? (y/N): `, (a) => { rl.close(); resolve(a); });
227
+ });
228
+
229
+ if (answer.toLowerCase() === 'y') {
230
+ console.log(`\n Kept: ${filepath}`);
231
+ } else {
232
+ unlinkSync(filepath);
233
+ console.log(`\n Deleted: ${filename}`);
234
+ }
235
+
236
+ console.log(`\n Next: Connect to your AI coding tool and ask it to`);
237
+ console.log(` "scan ${filename} for security issues"\n`);
238
+ }
@@ -0,0 +1,273 @@
1
+ import { execFileSync } from "child_process";
2
+ import { readFileSync, existsSync, writeFileSync, copyFileSync, mkdirSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { homedir, platform } from "os";
5
+ import { fileURLToPath } from "url";
6
+
7
+ // Handle both ESM and CJS bundling (Smithery bundles to CJS)
8
+ let __dirname;
9
+ try {
10
+ __dirname = dirname(fileURLToPath(import.meta.url));
11
+ } catch {
12
+ __dirname = process.cwd();
13
+ }
14
+
15
+ function vscodeBase() {
16
+ const os = platform();
17
+ if (os === 'darwin') return join(homedir(), 'Library', 'Application Support');
18
+ if (os === 'win32') return process.env.APPDATA || homedir();
19
+ return join(homedir(), '.config');
20
+ }
21
+
22
+ const MCP_SERVER_ENTRY = {
23
+ command: "npx",
24
+ args: ["-y", "agent-security-scanner-mcp"]
25
+ };
26
+
27
+ const CLIENT_CONFIGS = {
28
+ 'claude-desktop': {
29
+ name: 'Claude Desktop',
30
+ configKey: 'mcpServers',
31
+ configPath: () => {
32
+ const os = platform();
33
+ if (os === 'darwin') return join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
34
+ if (os === 'win32') return join(process.env.APPDATA || homedir(), 'Claude', 'claude_desktop_config.json');
35
+ return join(homedir(), '.config', 'Claude', 'claude_desktop_config.json');
36
+ },
37
+ buildEntry: () => ({ ...MCP_SERVER_ENTRY })
38
+ },
39
+ 'claude-code': {
40
+ name: 'Claude Code',
41
+ configKey: 'mcpServers',
42
+ configPath: () => join(homedir(), '.claude', 'settings.json'),
43
+ buildEntry: () => ({ ...MCP_SERVER_ENTRY })
44
+ },
45
+ 'cursor': {
46
+ name: 'Cursor',
47
+ configKey: 'mcpServers',
48
+ configPath: () => join(homedir(), '.cursor', 'mcp.json'),
49
+ buildEntry: () => ({ ...MCP_SERVER_ENTRY })
50
+ },
51
+ 'windsurf': {
52
+ name: 'Windsurf',
53
+ configKey: 'mcpServers',
54
+ configPath: () => {
55
+ const os = platform();
56
+ if (os === 'darwin') return join(homedir(), '.codeium', 'windsurf', 'mcp_config.json');
57
+ if (os === 'win32') return join(process.env.APPDATA || homedir(), '.codeium', 'windsurf', 'mcp_config.json');
58
+ return join(homedir(), '.codeium', 'windsurf', 'mcp_config.json');
59
+ },
60
+ buildEntry: () => ({ ...MCP_SERVER_ENTRY })
61
+ },
62
+ 'cline': {
63
+ name: 'Cline',
64
+ configKey: 'mcpServers',
65
+ configPath: () => join(vscodeBase(), 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'),
66
+ buildEntry: () => ({ ...MCP_SERVER_ENTRY })
67
+ },
68
+ 'kilo-code': {
69
+ name: 'Kilo Code',
70
+ configKey: 'mcpServers',
71
+ configPath: () => join(vscodeBase(), 'Code', 'User', 'globalStorage', 'kilocode.kilo-code', 'settings', 'mcp_settings.json'),
72
+ buildEntry: () => ({ ...MCP_SERVER_ENTRY, alwaysAllow: ["scan_security", "scan_agent_prompt", "check_package"], disabled: false })
73
+ },
74
+ 'opencode': {
75
+ name: 'OpenCode',
76
+ configKey: 'mcp',
77
+ configPath: () => join(process.cwd(), 'opencode.jsonc'),
78
+ buildEntry: () => ({ type: "local", command: ["npx", "-y", "agent-security-scanner-mcp"], enabled: true })
79
+ },
80
+ 'cody': {
81
+ name: 'Cody (Sourcegraph)',
82
+ configKey: 'mcpServers',
83
+ configPath: () => join(vscodeBase(), 'Code', 'User', 'globalStorage', 'sourcegraph.cody-ai', 'mcp_settings.json'),
84
+ buildEntry: () => ({ ...MCP_SERVER_ENTRY })
85
+ }
86
+ };
87
+
88
+ // Timestamp for backup filenames
89
+ function backupTimestamp() {
90
+ const d = new Date();
91
+ const pad = (n) => String(n).padStart(2, '0');
92
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
93
+ }
94
+
95
+ function checkCommand(cmd, args) {
96
+ try {
97
+ const out = execFileSync(cmd, args, { timeout: 10000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
98
+ return { ok: true, output: out.trim() };
99
+ } catch {
100
+ return { ok: false, output: null };
101
+ }
102
+ }
103
+
104
+ export async function runDoctor(args) {
105
+ const fix = args.includes('--fix') || false;
106
+ let issues = 0;
107
+ let fixed = 0;
108
+
109
+ console.log('\n agent-security-scanner-mcp doctor\n');
110
+
111
+ // --- Environment checks ---
112
+ console.log(' Environment');
113
+
114
+ // 1. Node version
115
+ const nodeVer = process.versions.node;
116
+ const nodeMajor = parseInt(nodeVer.split('.')[0], 10);
117
+ if (nodeMajor >= 18) {
118
+ console.log(` \u2713 Node.js v${nodeVer} (>= 18 required)`);
119
+ } else {
120
+ console.log(` \u2717 Node.js v${nodeVer} — version 18+ required`);
121
+ console.log(` Install: https://nodejs.org/`);
122
+ issues++;
123
+ }
124
+
125
+ // 2. Python 3
126
+ let pythonCmd = null;
127
+ const py3 = checkCommand('python3', ['--version']);
128
+ if (py3.ok) {
129
+ pythonCmd = 'python3';
130
+ console.log(` \u2713 ${py3.output}`);
131
+ } else {
132
+ const py = checkCommand('python', ['--version']);
133
+ if (py.ok && py.output.includes('3.')) {
134
+ pythonCmd = 'python';
135
+ console.log(` \u2713 ${py.output}`);
136
+ } else {
137
+ console.log(` \u2717 Python 3 not found`);
138
+ console.log(` Install: https://python.org/downloads/`);
139
+ issues++;
140
+ }
141
+ }
142
+
143
+ // 3. analyzer.py reachable
144
+ const analyzerPath = join(__dirname, '..', '..', 'analyzer.py');
145
+ if (existsSync(analyzerPath)) {
146
+ console.log(` \u2713 analyzer.py found`);
147
+ } else {
148
+ console.log(` \u2717 analyzer.py not found at ${analyzerPath}`);
149
+ console.log(` Try reinstalling: npm install -g agent-security-scanner-mcp`);
150
+ issues++;
151
+ }
152
+
153
+ // 4. Python can import yaml (analyzer dependency check)
154
+ if (pythonCmd && existsSync(analyzerPath)) {
155
+ const yamlCheck = checkCommand(pythonCmd, ['-c', 'import yaml; print("ok")']);
156
+ if (yamlCheck.ok && yamlCheck.output === 'ok') {
157
+ console.log(` \u2713 Analyzer engine ready (PyYAML installed)`);
158
+ } else {
159
+ // PyYAML missing but analyzer has fallback rules - still works
160
+ console.log(` \u2713 Analyzer engine ready (using fallback rules)`);
161
+ }
162
+ }
163
+
164
+ // 5. tree-sitter AST engine (optional but recommended)
165
+ if (pythonCmd) {
166
+ const tsCheck = checkCommand(pythonCmd, ['-c', 'import tree_sitter; print(tree_sitter.__version__)']);
167
+ if (tsCheck.ok && tsCheck.output) {
168
+ console.log(` \u2713 AST engine ready (tree-sitter ${tsCheck.output})`);
169
+ } else {
170
+ console.log(` \u26a0 tree-sitter not installed (regex-only mode)`);
171
+ console.log(` For enhanced detection: pip install tree-sitter tree-sitter-python tree-sitter-javascript`);
172
+ }
173
+ }
174
+
175
+ // --- Client configuration checks ---
176
+ console.log('\n Client Configurations');
177
+
178
+ for (const [key, client] of Object.entries(CLIENT_CONFIGS)) {
179
+ let configPath;
180
+ try { configPath = client.configPath(); } catch { continue; }
181
+
182
+ const configDir = dirname(configPath);
183
+
184
+ // Check if the tool appears installed (config dir exists)
185
+ if (!existsSync(configDir)) {
186
+ console.log(` \u2014 ${client.name.padEnd(20)} not installed (no config dir)`);
187
+ continue;
188
+ }
189
+
190
+ // Config file exists?
191
+ if (!existsSync(configPath)) {
192
+ console.log(` \u2717 ${client.name.padEnd(20)} config file not found: ${configPath}`);
193
+ if (fix) {
194
+ // Auto-fix: run init for this client
195
+ const entry = client.buildEntry();
196
+ const config = { [client.configKey]: { 'security-scanner': entry } };
197
+ mkdirSync(dirname(configPath), { recursive: true });
198
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
199
+ console.log(` \u2713 Fixed: created config with security-scanner entry`);
200
+ fixed++;
201
+ } else {
202
+ console.log(` Fix: npx agent-security-scanner-mcp init ${key}`);
203
+ issues++;
204
+ }
205
+ continue;
206
+ }
207
+
208
+ // Valid JSON?
209
+ let config;
210
+ try {
211
+ const raw = readFileSync(configPath, 'utf-8');
212
+ // Only strip comments for .jsonc files (avoid breaking URLs with //)
213
+ let stripped = raw;
214
+ if (configPath.endsWith('.jsonc')) {
215
+ stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
216
+ }
217
+ config = JSON.parse(stripped);
218
+ } catch (e) {
219
+ console.log(` \u2717 ${client.name.padEnd(20)} invalid JSON in config`);
220
+ console.log(` Error: ${e.message}`);
221
+ issues++;
222
+ continue;
223
+ }
224
+
225
+ // Has config section?
226
+ const section = config[client.configKey];
227
+ if (!section) {
228
+ console.log(` \u2717 ${client.name.padEnd(20)} missing "${client.configKey}" section`);
229
+ if (fix) {
230
+ config[client.configKey] = { 'security-scanner': client.buildEntry() };
231
+ const backupPath = `${configPath}.bak-${backupTimestamp()}`;
232
+ copyFileSync(configPath, backupPath);
233
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
234
+ console.log(` \u2713 Fixed: added ${client.configKey} with security-scanner entry`);
235
+ fixed++;
236
+ } else {
237
+ console.log(` Fix: npx agent-security-scanner-mcp init ${key}`);
238
+ issues++;
239
+ }
240
+ continue;
241
+ }
242
+
243
+ // Has our entry? Check common key names
244
+ const ourEntry = section['security-scanner'] || section['agentic-security'] || section['agent-security-scanner-mcp'];
245
+ if (ourEntry) {
246
+ const entryName = section['security-scanner'] ? 'security-scanner' : section['agentic-security'] ? 'agentic-security' : 'agent-security-scanner-mcp';
247
+ console.log(` \u2713 ${client.name.padEnd(20)} configured (${entryName})`);
248
+ } else {
249
+ console.log(` \u2717 ${client.name.padEnd(20)} entry missing from config`);
250
+ if (fix) {
251
+ config[client.configKey]['security-scanner'] = client.buildEntry();
252
+ const backupPath = `${configPath}.bak-${backupTimestamp()}`;
253
+ copyFileSync(configPath, backupPath);
254
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
255
+ console.log(` \u2713 Fixed: added security-scanner entry`);
256
+ fixed++;
257
+ } else {
258
+ console.log(` Fix: npx agent-security-scanner-mcp init ${key}`);
259
+ issues++;
260
+ }
261
+ }
262
+ }
263
+
264
+ // Summary
265
+ console.log('');
266
+ if (issues === 0 && fixed === 0) {
267
+ console.log(' All checks passed. You\'re good to go!\n');
268
+ } else if (fixed > 0) {
269
+ console.log(` Fixed ${fixed} issue(s). ${issues > 0 ? `${issues} remaining issue(s) need manual attention.` : 'All clear!'}\n`);
270
+ } else {
271
+ console.log(` ${issues} issue(s) found. Run with --fix to auto-repair, or use init <client>.\n`);
272
+ }
273
+ }