correctover-scan 1.0.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 ADDED
@@ -0,0 +1,94 @@
1
+ # correctover-scan
2
+
3
+ > CCS Security Scanner for MCP configurations — 14 security checks mapped to OWASP AISVS 1.0
4
+
5
+ ![npm](https://img.shields.io/npm/v/correctover-scan)
6
+ ![license](https://img.shields.io/npm/l/correctover-scan)
7
+
8
+ Scan your AI Agent MCP configuration files for security issues. Detects credential exposure, SSRF vulnerabilities, missing auth, and 11 more security checks.
9
+
10
+ ## Quick Start
11
+
12
+ ```bash
13
+ # Scan a specific config file
14
+ npx correctover-scan mcp.json
15
+
16
+ # Auto-detect and scan all MCP configs in current directory
17
+ npx correctover-scan
18
+
19
+ # Scan a directory
20
+ npx correctover-scan -d ./my-project
21
+ ```
22
+
23
+ ## What It Checks
24
+
25
+ | # | Check | Severity | AISVS |
26
+ |---|-------|----------|-------|
27
+ | 1 | TLS Transport Encryption | 🔴 Critical | C10.1 |
28
+ | 2 | Server Authentication | 🟠 High | C10.2 |
29
+ | 3 | Timeout Configuration | 🟡 Medium | C9.1 |
30
+ | 4 | Credential Exposure | 🔴 Critical | C5.1 |
31
+ | 5 | Tool Allowlist | 🟠 High | C9.3 |
32
+ | 6 | Token Budget Control | 🟠 High | C9.1 |
33
+ | 7 | SSRF Protection | 🔴 Critical | C10.3 |
34
+ | 8 | Audit Logging | 🟡 Medium | C12.1 |
35
+ | 9 | Sandbox Isolation | 🟡 Medium | C4.1 |
36
+ | 10 | Dependency Version Pinning | 🟡 Medium | C6.1 |
37
+ | 11 | Error Handling Strategy | 🟡 Medium | C12.2 |
38
+ | 12 | Input Validation | 🟠 High | C2.1 |
39
+ | 13 | Output Validation | 🟡 Medium | C7.1 |
40
+ | 14 | Kill Switch | 🟠 High | C9.5 |
41
+
42
+ ## Output Formats
43
+
44
+ ```bash
45
+ # Terminal (default)
46
+ correctover-scan mcp.json
47
+
48
+ # JSON
49
+ correctover-scan mcp.json -f json
50
+
51
+ # SARIF (for CI integration)
52
+ correctover-scan mcp.json -f sarif > report.sarif
53
+ ```
54
+
55
+ ## Supported Config Files
56
+
57
+ Auto-detects these files:
58
+ - `.cursor/mcp.json`
59
+ - `claude_desktop_config.json`
60
+ - `.claude/mcp.json`
61
+ - `mcp.json` / `mcp.yaml` / `mcp.yml`
62
+ - `.vscode/mcp.json`
63
+ - `.mcp/mcp.json`
64
+ - `config/mcp.json`
65
+
66
+ ## CI/CD Integration
67
+
68
+ ### GitHub Actions
69
+
70
+ ```yaml
71
+ - uses: Correctover/correctover-scan-action@v1
72
+ with:
73
+ path: ./mcp.json
74
+ ```
75
+
76
+ ### Web Scanner
77
+
78
+ Try the online version: [correctover.com/scan](https://correctover.com/scan/)
79
+
80
+ ## Standards Compliance
81
+
82
+ - **OWASP AISVS 1.0** — AI System Vulnerability Severity
83
+ - **GB/T《智能体应用安全基本要求》** — Chinese National Mandatory Standard
84
+
85
+ ## Links
86
+
87
+ - [correctover.com](https://correctover.com) — AI Agent Runtime Assurance
88
+ - [CCS Standard](https://correctover.com/ccs) — Conformance Specification
89
+ - [Web Scanner](https://correctover.com/scan/) — Online version
90
+ - [GitHub](https://github.com/Correctover) — Source code
91
+
92
+ ## License
93
+
94
+ MIT © Correctover
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Correctover CCS Security Scanner - Core Engine
3
+ * 14 security checks mapped to OWASP AISVS 1.0
4
+ * Shared by CLI / GitHub Action / VS Code Extension
5
+ */
6
+
7
+ const CHECKS = [
8
+ {
9
+ id: 'mcp-tls', category: 'C10 MCP安全', name: 'TLS传输加密', severity: 'critical',
10
+ check: (cfg) => {
11
+ const servers = getServers(cfg);
12
+ if (!servers.length) return 'info';
13
+ return servers.every(s => !s.url || s.url.startsWith('https://') || s.url.startsWith('stdio:')) ? 'pass' : 'fail';
14
+ },
15
+ fix: '所有MCP Server URL应使用HTTPS协议,禁止明文HTTP传输',
16
+ aisvs: 'C10.1'
17
+ },
18
+ {
19
+ id: 'mcp-auth', category: 'C10 MCP安全', name: '服务器鉴权配置', severity: 'high',
20
+ check: (cfg) => {
21
+ const servers = getServers(cfg);
22
+ if (!servers.length) return 'info';
23
+ const hasAuth = servers.some(s => s.headers?.authorization || s.headers?.Authorization || s.env?.API_KEY);
24
+ return hasAuth ? 'pass' : 'warn';
25
+ },
26
+ fix: '为MCP Server配置认证头(Authorization/API Key),防止未授权访问',
27
+ aisvs: 'C10.2'
28
+ },
29
+ {
30
+ id: 'mcp-timeout', category: 'C9 Agent安全', name: '超时配置', severity: 'medium',
31
+ check: (cfg) => {
32
+ return JSON.stringify(cfg).includes('timeout') ? 'pass' : 'warn';
33
+ },
34
+ fix: '为MCP Server连接设置超时时间,防止Agent因无响应Server而挂起',
35
+ aisvs: 'C9.1'
36
+ },
37
+ {
38
+ id: 'cred-exposure', category: 'C5 访问控制', name: '凭证明文暴露', severity: 'critical',
39
+ check: (cfg) => {
40
+ const str = JSON.stringify(cfg);
41
+ const patterns = [/sk-[a-zA-Z0-9]{20,}/, /AKIA[A-Z0-9]{16}/, /ghp_[a-zA-Z0-9]{36}/, /password\s*:\s*["'][^"']+["']/i];
42
+ return patterns.some(p => p.test(str)) ? 'fail' : 'pass';
43
+ },
44
+ fix: '禁止在配置文件中硬编码API密钥。使用环境变量引用(如 ${API_KEY})或密钥管理服务',
45
+ aisvs: 'C5.1'
46
+ },
47
+ {
48
+ id: 'allowed-tools', category: 'C9 Agent安全', name: '工具白名单', severity: 'high',
49
+ check: (cfg) => {
50
+ const str = JSON.stringify(cfg);
51
+ return (str.includes('allowed_tools') || str.includes('allowedTools') || str.includes('permissions')) ? 'pass' : 'warn';
52
+ },
53
+ fix: '配置allowed_tools白名单,限制Agent可调用的工具范围,遵循最小权限原则',
54
+ aisvs: 'C9.3'
55
+ },
56
+ {
57
+ id: 'budget-limit', category: 'C9 Agent安全', name: 'Token预算控制', severity: 'high',
58
+ check: (cfg) => {
59
+ const str = JSON.stringify(cfg);
60
+ return (str.includes('budget') || str.includes('max_tokens') || str.includes('token_limit') || str.includes('cost_limit')) ? 'pass' : 'warn';
61
+ },
62
+ fix: '设置Token消耗预算上限,防止Agent因循环调用导致成本失控',
63
+ aisvs: 'C9.1'
64
+ },
65
+ {
66
+ id: 'ssrf-protection', category: 'C10 MCP安全', name: 'SSRF防护', severity: 'critical',
67
+ check: (cfg) => {
68
+ const servers = getServers(cfg);
69
+ const urls = servers.map(s => s.url || '').join(' ');
70
+ const internal = /169\.254\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|localhost|127\.0\.0\.1/i;
71
+ return internal.test(urls) ? 'fail' : 'pass';
72
+ },
73
+ fix: 'MCP Server URL不应指向内网地址(169.254.x.x/10.x/172.16-31.x/192.168.x),防止SSRF攻击',
74
+ aisvs: 'C10.3'
75
+ },
76
+ {
77
+ id: 'logging', category: 'C12 监控', name: '审计日志配置', severity: 'medium',
78
+ check: (cfg) => {
79
+ const str = JSON.stringify(cfg);
80
+ return (str.includes('log') || str.includes('audit') || str.includes('trace') || str.includes('telemetry')) ? 'pass' : 'warn';
81
+ },
82
+ fix: '启用审计日志记录所有Agent操作,便于事后追溯和安全分析',
83
+ aisvs: 'C12.1'
84
+ },
85
+ {
86
+ id: 'sandbox', category: 'C4 基础设施', name: '沙箱隔离配置', severity: 'medium',
87
+ check: (cfg) => {
88
+ const str = JSON.stringify(cfg);
89
+ return (str.includes('sandbox') || str.includes('isolation') || str.includes('container') || str.includes('docker')) ? 'pass' : 'info';
90
+ },
91
+ fix: '考虑为MCP Server配置沙箱执行环境,限制文件系统和网络访问',
92
+ aisvs: 'C4.1'
93
+ },
94
+ {
95
+ id: 'version-pin', category: 'C6 供应链', name: '依赖版本锁定', severity: 'medium',
96
+ check: (cfg) => {
97
+ const servers = getServers(cfg);
98
+ if (!servers.length) return 'info';
99
+ return servers.some(s => s.command || s.version) ? 'pass' : 'warn';
100
+ },
101
+ fix: '锁定MCP Server依赖的具体版本号,防止供应链攻击',
102
+ aisvs: 'C6.1'
103
+ },
104
+ {
105
+ id: 'error-handling', category: 'C12 监控', name: '错误处理策略', severity: 'medium',
106
+ check: (cfg) => {
107
+ const str = JSON.stringify(cfg);
108
+ return (str.includes('retry') || str.includes('fallback') || str.includes('error') || str.includes('on_error')) ? 'pass' : 'warn';
109
+ },
110
+ fix: '配置错误处理和故障转移策略,确保Agent在Server故障时优雅降级',
111
+ aisvs: 'C12.2'
112
+ },
113
+ {
114
+ id: 'input-validation', category: 'C2 输入验证', name: '输入校验规则', severity: 'high',
115
+ check: (cfg) => {
116
+ const str = JSON.stringify(cfg);
117
+ return (str.includes('validation') || str.includes('schema') || str.includes('input_check') || str.includes('sanitize')) ? 'pass' : 'warn';
118
+ },
119
+ fix: '为Agent输入配置校验规则,防御提示注入和编码走私攻击',
120
+ aisvs: 'C2.1'
121
+ },
122
+ {
123
+ id: 'output-validation', category: 'C7 输出控制', name: '输出校验规则', severity: 'medium',
124
+ check: (cfg) => {
125
+ const str = JSON.stringify(cfg);
126
+ return (str.includes('output') || str.includes('response_check') || str.includes('filter')) ? 'pass' : 'info';
127
+ },
128
+ fix: '对Agent输出进行格式校验和敏感信息过滤',
129
+ aisvs: 'C7.1'
130
+ },
131
+ {
132
+ id: 'kill-switch', category: 'C9 Agent安全', name: '紧急终止机制', severity: 'high',
133
+ check: (cfg) => {
134
+ const str = JSON.stringify(cfg);
135
+ return (str.includes('kill') || str.includes('circuit_breaker') || str.includes('emergency') || str.includes('abort')) ? 'pass' : 'warn';
136
+ },
137
+ fix: '配置紧急终止开关(kill switch),在检测到异常行为时立即停止Agent',
138
+ aisvs: 'C9.5'
139
+ }
140
+ ];
141
+
142
+ function getServers(cfg) {
143
+ if (cfg.mcpServers) return Object.values(cfg.mcpServers);
144
+ if (cfg.servers) return cfg.servers;
145
+ if (Array.isArray(cfg)) return cfg;
146
+ return [];
147
+ }
148
+
149
+ /**
150
+ * Run security scan on a parsed config object
151
+ * @param {Object} config - Parsed MCP config
152
+ * @returns {{ results: Array, stats: Object }}
153
+ */
154
+ function runScan(config) {
155
+ const results = CHECKS.map(check => {
156
+ let status;
157
+ try { status = check.check(config); } catch (e) { status = 'info'; }
158
+ return { ...check, status };
159
+ });
160
+
161
+ const pass = results.filter(r => r.status === 'pass').length;
162
+ const warn = results.filter(r => r.status === 'warn').length;
163
+ const fail = results.filter(r => r.status === 'fail').length;
164
+ const info = results.filter(r => r.status === 'info').length;
165
+ const total = results.length;
166
+ const score = Math.round(((pass * 10 + warn * 5 + info * 7) / (total * 10)) * 100);
167
+
168
+ return { results, stats: { pass, warn, fail, info, total, score } };
169
+ }
170
+
171
+ /**
172
+ * Parse config content (JSON or simple YAML)
173
+ * @param {string} content - File content
174
+ * @param {string} filename - Original filename
175
+ * @returns {Object} Parsed config
176
+ */
177
+ function parseConfig(content, filename = '') {
178
+ if (filename.endsWith('.json') || filename.endsWith('.toml')) {
179
+ return JSON.parse(content);
180
+ }
181
+ // Try JSON first for unknown extensions
182
+ try { return JSON.parse(content); } catch (e) {}
183
+ // Basic YAML parse
184
+ return parseSimpleYAML(content);
185
+ }
186
+
187
+ function parseSimpleYAML(text) {
188
+ const result = {};
189
+ const lines = text.split('\n');
190
+ let currentPath = [];
191
+ let indentStack = [-1];
192
+ for (const line of lines) {
193
+ if (!line.trim() || line.trim().startsWith('#')) continue;
194
+ const indent = line.search(/\S/);
195
+ const match = line.trim().match(/^([^:]+):\s*(.*)/);
196
+ if (!match) continue;
197
+ const key = match[1].trim();
198
+ let val = match[2].trim();
199
+ while (indentStack.length > 1 && indent <= indentStack[indentStack.length - 1]) {
200
+ indentStack.pop();
201
+ currentPath.pop();
202
+ }
203
+ if (val === '' || val === '{}' || val === '[]') {
204
+ currentPath.push(key);
205
+ indentStack.push(indent);
206
+ } else {
207
+ val = val.replace(/^["']|["']$/g, '');
208
+ setNestedValue(result, [...currentPath, key], val);
209
+ }
210
+ }
211
+ return result;
212
+ }
213
+
214
+ function setNestedValue(obj, path, value) {
215
+ let current = obj;
216
+ for (let i = 0; i < path.length - 1; i++) {
217
+ if (!current[path[i]]) current[path[i]] = {};
218
+ current = current[path[i]];
219
+ }
220
+ current[path[path.length - 1]] = value;
221
+ }
222
+
223
+ // Auto-detect MCP config files in a directory
224
+ const KNOWN_CONFIG_PATHS = [
225
+ '.cursor/mcp.json',
226
+ 'claude_desktop_config.json',
227
+ '.claude/mcp.json',
228
+ 'mcp.json',
229
+ 'mcp.yaml',
230
+ 'mcp.yml',
231
+ '.vscode/mcp.json',
232
+ 'config/mcp.json',
233
+ '.mcp/mcp.json',
234
+ 'mcp_config.json',
235
+ ];
236
+
237
+ if (typeof module !== 'undefined' && module.exports) {
238
+ module.exports = { CHECKS, runScan, parseConfig, getServers, KNOWN_CONFIG_PATHS };
239
+ }
package/index.js ADDED
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * correctover-scan — CCS Security Scanner for MCP Configurations
4
+ * Usage: npx correctover-scan [config-file] [options]
5
+ *
6
+ * Scans MCP configuration files for security issues.
7
+ * Maps to OWASP AISVS 1.0 and Chinese National Standard《智能体应用安全基本要求》
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { runScan, parseConfig, KNOWN_CONFIG_PATHS } = require('./core/scanner');
13
+
14
+ const VERSION = '1.0.0';
15
+
16
+ // Colors
17
+ const c = {
18
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
19
+ red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m',
20
+ cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m',
21
+ bgRed: '\x1b[41m', bgGreen: '\x1b[42m', bgYellow: '\x1b[43m',
22
+ };
23
+
24
+ const icons = { pass: '✅', warn: '⚠️', fail: '❌', info: 'ℹ️' };
25
+ const sevColors = { critical: c.red, high: c.yellow, medium: c.cyan, low: c.gray };
26
+ const sevLabels = { critical: 'CRITICAL', high: 'HIGH', medium: 'MEDIUM', low: 'LOW' };
27
+
28
+ function printBanner() {
29
+ console.log(`
30
+ ${c.bold}${c.blue} ╔══════════════════════════════════════════╗
31
+ ║ CCS Security Scanner by Correctover ║
32
+ ║ AI Agent Runtime Assurance ║
33
+ ╚══════════════════════════════════════════╝${c.reset}
34
+ ${c.dim}v${VERSION} | OWASP AISVS 1.0 | 14 security checks${c.reset}
35
+ `);
36
+ }
37
+
38
+ function findConfigFiles(dir) {
39
+ const found = [];
40
+ for (const relPath of KNOWN_CONFIG_PATHS) {
41
+ const fullPath = path.resolve(dir, relPath);
42
+ if (fs.existsSync(fullPath)) {
43
+ found.push(fullPath);
44
+ }
45
+ }
46
+ // Also scan for any mcp*.json or mcp*.yaml in common locations
47
+ const scanDirs = ['.', '.cursor', '.claude', '.vscode', '.mcp', 'config'];
48
+ for (const d of scanDirs) {
49
+ const dirPath = path.resolve(dir, d);
50
+ if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
51
+ try {
52
+ const files = fs.readdirSync(dirPath);
53
+ for (const f of files) {
54
+ if (/^mcp[_-]?.*\.(json|yaml|yml)$/.test(f)) {
55
+ const fp = path.join(dirPath, f);
56
+ if (!found.includes(fp)) found.push(fp);
57
+ }
58
+ }
59
+ } catch (e) {}
60
+ }
61
+ }
62
+ return found;
63
+ }
64
+
65
+ function formatResults(results, stats, filename) {
66
+ const lines = [];
67
+
68
+ // File header
69
+ lines.push(`${c.bold}📄 ${filename}${c.reset}`);
70
+ lines.push('─'.repeat(50));
71
+
72
+ // Score
73
+ const scoreColor = stats.score >= 80 ? c.green : stats.score >= 60 ? c.yellow : c.red;
74
+ lines.push(`\n${c.bold}Security Score: ${scoreColor}${c.bold}${stats.score}/100${c.reset}`);
75
+ lines.push(` ${c.green}✓ ${stats.pass} passed${c.reset} ${c.yellow}⚠ ${stats.warn} warnings${c.reset} ${c.red}✗ ${stats.fail} critical${c.reset} ${c.blue}ℹ ${stats.info} info${c.reset}\n`);
76
+
77
+ // Group by category
78
+ const categories = {};
79
+ for (const r of results) {
80
+ if (!categories[r.category]) categories[r.category] = [];
81
+ categories[r.category].push(r);
82
+ }
83
+
84
+ for (const [cat, checks] of Object.entries(categories)) {
85
+ lines.push(`${c.dim}── ${cat} ──${c.reset}`);
86
+ for (const r of checks) {
87
+ const icon = icons[r.status];
88
+ const sevColor = sevColors[r.severity];
89
+ const sevLabel = sevLabels[r.severity];
90
+ const statusStr = r.status === 'pass' ? c.green + 'PASS' : r.status === 'fail' ? c.red + 'FAIL' : r.status === 'warn' ? c.yellow + 'WARN' : c.blue + 'INFO';
91
+ lines.push(` ${icon} ${r.name} ${c.reset}[${statusStr}${c.reset}] ${c.dim}${r.aisvs}${c.reset}`);
92
+ }
93
+ lines.push('');
94
+ }
95
+
96
+ // Recommendations
97
+ const issues = results.filter(r => r.status === 'fail' || r.status === 'warn');
98
+ if (issues.length > 0) {
99
+ lines.push(`${c.bold}${c.yellow}Recommendations:${c.reset}\n`);
100
+ for (const r of issues) {
101
+ const icon = r.status === 'fail' ? '🔴' : '🟡';
102
+ lines.push(` ${icon} ${c.bold}${r.name}${c.reset}`);
103
+ lines.push(` ${c.gray}${r.fix}${c.reset}\n`);
104
+ }
105
+ } else {
106
+ lines.push(`${c.green}${c.bold}🎉 All checks passed! Your MCP configuration is secure.${c.reset}\n`);
107
+ }
108
+
109
+ // CTA
110
+ lines.push(`${c.dim}── ${c.reset}`);
111
+ lines.push(`${c.dim}Try the web scanner: https://correctover.com/scan/${c.reset}`);
112
+ lines.push(`${c.dim}Learn more: https://correctover.com${c.reset}`);
113
+ lines.push(`${c.dim}GitHub: https://github.com/Correctover${c.reset}`);
114
+
115
+ return lines.join('\n');
116
+ }
117
+
118
+ function formatSARIF(results, filename) {
119
+ return {
120
+ version: '2.1.0',
121
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
122
+ runs: [{
123
+ tool: {
124
+ driver: {
125
+ name: 'correctover-scan',
126
+ version: VERSION,
127
+ informationUri: 'https://correctover.com',
128
+ rules: results.map(r => ({
129
+ id: r.id,
130
+ name: r.name,
131
+ shortDescription: { text: `${r.category}: ${r.name}` },
132
+ helpUri: `https://correctover.com/scan/#check-${r.id}`,
133
+ properties: { aisvs: r.aisvs, severity: r.severity }
134
+ }))
135
+ }
136
+ },
137
+ results: results.filter(r => r.status === 'fail' || r.status === 'warn').map(r => ({
138
+ ruleId: r.id,
139
+ level: r.status === 'fail' ? 'error' : 'warning',
140
+ message: { text: r.fix },
141
+ locations: [{ physicalLocation: { artifactLocation: { uri: filename } } }]
142
+ }))
143
+ }]
144
+ };
145
+ }
146
+
147
+ function formatJSON(results, stats, filename) {
148
+ return JSON.stringify({ scanner: 'correctover-scan', version: VERSION, file: filename, stats, results }, null, 2);
149
+ }
150
+
151
+ function main() {
152
+ const args = process.argv.slice(2);
153
+ let configPath = null;
154
+ let format = 'text'; // text, json, sarif
155
+ let scanDir = process.cwd();
156
+ let recursive = false;
157
+
158
+ // Parse args
159
+ for (let i = 0; i < args.length; i++) {
160
+ const arg = args[i];
161
+ if (arg === '--version' || arg === '-v') {
162
+ console.log(`correctover-scan v${VERSION}`);
163
+ process.exit(0);
164
+ } else if (arg === '--help' || arg === '-h') {
165
+ console.log(`
166
+ Usage: correctover-scan [config-file] [options]
167
+
168
+ Options:
169
+ -f, --format <type> Output format: text, json, sarif (default: text)
170
+ -d, --dir <path> Directory to scan for MCP configs (default: cwd)
171
+ -r, --recursive Recursively find MCP config files
172
+ -v, --version Show version
173
+ -h, --help Show this help
174
+
175
+ Examples:
176
+ correctover-scan mcp.json Scan a specific file
177
+ correctover-scan -d ./project Auto-detect configs in directory
178
+ correctover-scan -f sarif -o report.sarif Output SARIF format
179
+ npx correctover-scan Auto-detect in current directory
180
+ `);
181
+ process.exit(0);
182
+ } else if (arg === '--format' || arg === '-f') {
183
+ format = args[++i];
184
+ } else if (arg === '--dir' || arg === '-d') {
185
+ scanDir = args[++i];
186
+ } else if (arg === '--recursive' || arg === '-r') {
187
+ recursive = true;
188
+ } else if (arg === '--output' || arg === '-o') {
189
+ // Output file - handled below
190
+ } else if (!arg.startsWith('-')) {
191
+ configPath = arg;
192
+ }
193
+ }
194
+
195
+ printBanner();
196
+
197
+ const filesToScan = [];
198
+
199
+ if (configPath) {
200
+ // Specific file
201
+ if (!fs.existsSync(configPath)) {
202
+ console.error(`${c.red}Error: File not found: ${configPath}${c.reset}`);
203
+ process.exit(1);
204
+ }
205
+ filesToScan.push(configPath);
206
+ } else {
207
+ // Auto-detect
208
+ console.log(`${c.dim}Auto-detecting MCP configs in ${scanDir}...${c.reset}\n`);
209
+ const found = findConfigFiles(scanDir);
210
+ if (recursive) {
211
+ // Walk directory tree
212
+ function walkDir(dir) {
213
+ try {
214
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
215
+ for (const entry of entries) {
216
+ if (entry.name.startsWith('.') && entry.name !== '.cursor' && entry.name !== '.claude' && entry.name !== '.vscode' && entry.name !== '.mcp') continue;
217
+ const fullPath = path.join(dir, entry.name);
218
+ if (entry.isDirectory() && entry.name !== 'node_modules') {
219
+ walkDir(fullPath);
220
+ } else if (entry.isFile() && /mcp[_-]?.*\.(json|yaml|yml)$/.test(entry.name)) {
221
+ if (!found.includes(fullPath)) found.push(fullPath);
222
+ }
223
+ }
224
+ } catch (e) {}
225
+ }
226
+ walkDir(scanDir);
227
+ }
228
+ filesToScan.push(...found);
229
+ }
230
+
231
+ if (filesToScan.length === 0) {
232
+ console.log(`${c.yellow}No MCP configuration files found.${c.reset}`);
233
+ console.log(`${c.dim}Searched paths: ${KNOWN_CONFIG_PATHS.join(', ')}${c.reset}`);
234
+ console.log(`\n${c.dim}Create a config file or specify one: correctover-scan path/to/mcp.json${c.reset}`);
235
+ process.exit(0);
236
+ }
237
+
238
+ console.log(`${c.dim}Found ${filesToScan.length} config file(s)${c.reset}\n`);
239
+
240
+ let totalPass = 0, totalWarn = 0, totalFail = 0, totalInfo = 0;
241
+ let allResults = [];
242
+
243
+ for (const fp of filesToScan) {
244
+ try {
245
+ const content = fs.readFileSync(fp, 'utf-8');
246
+ const config = parseConfig(content, fp);
247
+ const { results, stats } = runScan(config);
248
+ const relPath = path.relative(process.cwd(), fp) || fp;
249
+
250
+ if (format === 'text') {
251
+ console.log(formatResults(results, stats, relPath));
252
+ console.log('');
253
+ }
254
+
255
+ totalPass += stats.pass;
256
+ totalWarn += stats.warn;
257
+ totalFail += stats.fail;
258
+ totalInfo += stats.info;
259
+ allResults.push({ file: relPath, results, stats });
260
+ } catch (e) {
261
+ console.error(`${c.red}Error scanning ${fp}: ${e.message}${c.reset}\n`);
262
+ }
263
+ }
264
+
265
+ // JSON/SARIF output
266
+ if (format === 'json') {
267
+ const output = allResults.map(r => formatJSON(r.results, r.stats, r.file));
268
+ console.log(output.join('\n'));
269
+ } else if (format === 'sarif') {
270
+ const sarifResults = allResults.map(r => formatSARIF(r.results, r.file));
271
+ console.log(JSON.stringify(sarifResults.length === 1 ? sarifResults[0] : { runs: sarifResults.flatMap(s => s.runs) }, null, 2));
272
+ }
273
+
274
+ // Summary
275
+ if (filesToScan.length > 1 && format === 'text') {
276
+ const totalScore = Math.round(((totalPass * 10 + totalWarn * 5 + totalInfo * 7) / ((totalPass + totalWarn + totalFail + totalInfo) * 10)) * 100);
277
+ console.log(`${c.bold}═══ Summary ═══${c.reset}`);
278
+ console.log(` Files scanned: ${filesToScan.length}`);
279
+ console.log(` Total score: ${totalScore}/100`);
280
+ console.log(` ${c.green}✓ ${totalPass}${c.reset} ${c.yellow}⚠ ${totalWarn}${c.reset} ${c.red}✗ ${totalFail}${c.reset} ${c.blue}ℹ ${totalInfo}${c.reset}`);
281
+ }
282
+
283
+ // Exit code: 1 if any critical failures
284
+ if (totalFail > 0) process.exit(1);
285
+ }
286
+
287
+ main();
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "correctover-scan",
3
+ "version": "1.0.0",
4
+ "description": "CCS Security Scanner for MCP configurations — scan Agent security configs against OWASP AISVS 1.0",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "correctover-scan": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node test.js",
11
+ "prepublishOnly": "node test.js"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "security",
16
+ "scanner",
17
+ "agent",
18
+ "ai",
19
+ "owasp",
20
+ "aisvs",
21
+ "ccs",
22
+ "correctover",
23
+ "claude",
24
+ "cursor",
25
+ "model-context-protocol",
26
+ "vulnerability",
27
+ "audit"
28
+ ],
29
+ "author": "Correctover",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/Correctover/correctover-scan"
34
+ },
35
+ "homepage": "https://correctover.com/scan",
36
+ "bugs": {
37
+ "url": "https://github.com/Correctover/correctover-scan/issues"
38
+ },
39
+ "files": [
40
+ "index.js",
41
+ "core/scanner.js",
42
+ "README.md"
43
+ ],
44
+ "engines": {
45
+ "node": ">=16.0.0"
46
+ }
47
+ }