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 +94 -0
- package/core/scanner.js +239 -0
- package/index.js +287 -0
- package/package.json +47 -0
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
|
+

|
|
6
|
+

|
|
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
|
package/core/scanner.js
ADDED
|
@@ -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
|
+
}
|