@getvetai/cli 0.2.0 โ 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -47
- package/package.json +3 -4
- package/dist/commands/audit.d.ts +0 -5
- package/dist/commands/audit.js +0 -45
- package/dist/commands/find.d.ts +0 -3
- package/dist/commands/find.js +0 -28
- package/dist/commands/install.d.ts +0 -5
- package/dist/commands/install.js +0 -44
- package/dist/commands/scan.d.ts +0 -6
- package/dist/commands/scan.js +0 -108
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -43
- package/dist/types.d.ts +0 -52
- package/dist/types.js +0 -1
- package/dist/utils/analyzer.d.ts +0 -3
- package/dist/utils/analyzer.js +0 -94
- package/dist/utils/api.d.ts +0 -3
- package/dist/utils/api.js +0 -46
- package/dist/utils/config.d.ts +0 -14
- package/dist/utils/config.js +0 -140
- package/dist/utils/display.d.ts +0 -16
- package/dist/utils/display.js +0 -145
- package/dist/utils/fetch.d.ts +0 -6
- package/dist/utils/fetch.js +0 -38
- package/dist/utils/mcp-analyzer.d.ts +0 -19
- package/dist/utils/mcp-analyzer.js +0 -187
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @getvetai/cli
|
|
2
2
|
|
|
3
|
-
Security audit CLI for AI skills and MCP servers. Scan, audit, and
|
|
3
|
+
Security audit CLI for AI skills and MCP servers. Scan, audit, and discover tools before you install them.
|
|
4
4
|
|
|
5
|
-
๐ **Registry:** [getvet.ai](https://getvet.ai) โ
|
|
5
|
+
๐ **Registry:** [getvet.ai](https://getvet.ai) โ 23,000+ AI tools verified and scored
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -10,58 +10,43 @@ Security audit CLI for AI skills and MCP servers. Scan, audit, and score tools b
|
|
|
10
10
|
npm install -g @getvetai/cli
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Or run without installing:
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- **`--deep` flag** โ request a deep scan from the registry
|
|
19
|
-
- **Better display** โ trust score bars, registry links, badge indicators
|
|
15
|
+
```bash
|
|
16
|
+
npx @getvetai/cli scan .
|
|
17
|
+
```
|
|
20
18
|
|
|
21
19
|
## Commands
|
|
22
20
|
|
|
23
21
|
### `vet scan <target>`
|
|
24
22
|
|
|
25
|
-
Scan a
|
|
26
|
-
|
|
27
|
-
For npm packages, the CLI first checks the getvet.ai registry for existing scan results. If a deep scan is available, it returns instantly without local analysis.
|
|
23
|
+
Scan a tool for security issues. Checks the [getvet.ai](https://getvet.ai) registry first for instant results.
|
|
28
24
|
|
|
29
25
|
```bash
|
|
30
26
|
# Scan an npm package (checks registry first)
|
|
31
27
|
vet scan @modelcontextprotocol/server-filesystem
|
|
32
28
|
|
|
33
|
-
#
|
|
29
|
+
# Local analysis only (skip registry)
|
|
34
30
|
vet scan @modelcontextprotocol/server-filesystem --offline
|
|
35
31
|
|
|
36
32
|
# Request a deep scan from registry
|
|
37
33
|
vet scan @modelcontextprotocol/server-filesystem --deep
|
|
38
34
|
|
|
39
|
-
# Scan a local
|
|
40
|
-
vet scan ./my-
|
|
35
|
+
# Scan a local project
|
|
36
|
+
vet scan ./my-mcp-server
|
|
41
37
|
|
|
42
38
|
# Scan a GitHub repo
|
|
43
39
|
vet scan https://github.com/modelcontextprotocol/servers
|
|
44
40
|
|
|
45
|
-
#
|
|
41
|
+
# JSON output
|
|
46
42
|
vet scan ./SKILL.md --json
|
|
47
43
|
```
|
|
48
44
|
|
|
49
|
-
**Output includes:** trust score, badge (certified/reviewed/unverified/flagged), detected permissions, security issues, risk factors, tools list, and registry link.
|
|
50
|
-
|
|
51
45
|
### `vet audit [path]`
|
|
52
46
|
|
|
53
|
-
Audit all AI tools in a project.
|
|
47
|
+
Audit all AI tools in a project. Auto-discovers MCP configurations from:
|
|
54
48
|
|
|
55
|
-
|
|
56
|
-
- Cursor (`.cursor/mcp.json`)
|
|
57
|
-
- Claude Desktop (`claude_desktop_config.json`)
|
|
58
|
-
- VS Code (`settings.json` โ `mcp.servers`)
|
|
59
|
-
- Windsurf (`mcp.json`)
|
|
60
|
-
- Cline (`mcp_settings.json`)
|
|
61
|
-
- Zed (`settings.json` โ `mcp`)
|
|
62
|
-
- Continue (`config.json` โ `mcpServers`)
|
|
63
|
-
- OpenClaw (`openclaw.json`)
|
|
64
|
-
- `SKILL.md` files
|
|
49
|
+
**Claude Desktop** ยท **Cursor** ยท **VS Code** ยท **Windsurf** ยท **Cline** ยท **Zed** ยท **Continue** ยท **OpenClaw**
|
|
65
50
|
|
|
66
51
|
```bash
|
|
67
52
|
# Audit current directory
|
|
@@ -70,7 +55,7 @@ vet audit
|
|
|
70
55
|
# Audit a specific project
|
|
71
56
|
vet audit ./my-project
|
|
72
57
|
|
|
73
|
-
# Strict mode โ exit
|
|
58
|
+
# Strict mode โ exit 1 if any tool is unverified/flagged
|
|
74
59
|
vet audit --strict
|
|
75
60
|
|
|
76
61
|
# JSON output
|
|
@@ -79,12 +64,18 @@ vet audit --json
|
|
|
79
64
|
|
|
80
65
|
### `vet find <query>`
|
|
81
66
|
|
|
82
|
-
Search the getvet.ai registry by description.
|
|
67
|
+
Search the getvet.ai registry for tools by description.
|
|
83
68
|
|
|
84
69
|
```bash
|
|
85
70
|
# Search for tools
|
|
86
|
-
vet find "
|
|
87
|
-
vet find "database
|
|
71
|
+
vet find "web scraping"
|
|
72
|
+
vet find "database access"
|
|
73
|
+
|
|
74
|
+
# Limit results
|
|
75
|
+
vet find "browser automation" --limit 20
|
|
76
|
+
|
|
77
|
+
# Filter by type
|
|
78
|
+
vet find "file management" --type mcp
|
|
88
79
|
|
|
89
80
|
# JSON output
|
|
90
81
|
vet find "weather" --json
|
|
@@ -92,36 +83,57 @@ vet find "weather" --json
|
|
|
92
83
|
|
|
93
84
|
### `vet install <package>`
|
|
94
85
|
|
|
95
|
-
Install a package with a pre-install security audit.
|
|
86
|
+
Install a package with a pre-install security audit.
|
|
96
87
|
|
|
97
88
|
```bash
|
|
98
|
-
# Audit + install
|
|
89
|
+
# Audit + install
|
|
99
90
|
vet install @modelcontextprotocol/server-github
|
|
100
91
|
|
|
101
92
|
# Install globally
|
|
102
93
|
vet install -g some-mcp-server
|
|
103
|
-
|
|
104
|
-
# Install as OpenClaw skill
|
|
105
|
-
vet install --skill weather
|
|
106
94
|
```
|
|
107
95
|
|
|
108
|
-
##
|
|
96
|
+
## Verification Levels
|
|
109
97
|
|
|
110
|
-
|
|
|
98
|
+
| Level | Badge | Meaning |
|
|
111
99
|
|-------|-------|---------|
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
115
|
-
| 0-24 | ๐ซ Flagged | Critical security issues found |
|
|
100
|
+
| L2 | โ
Verified | Installs, boots, tools discovered and tested |
|
|
101
|
+
| L1 | ๐ Boots | Installs and boots successfully |
|
|
102
|
+
| L0 | โ ๏ธ Indexed | Cataloged, not yet verified |
|
|
116
103
|
|
|
117
104
|
## What It Detects
|
|
118
105
|
|
|
119
|
-
**Permissions:** shell execution, file
|
|
106
|
+
- **Permissions:** shell execution, file I/O, network access, browser control, database queries, crypto operations
|
|
107
|
+
- **Security issues:** destructive commands, remote code execution, dynamic eval, credential patterns, elevated privileges
|
|
108
|
+
- **MCP-specific:** tool parameter analysis, transport detection (stdio/http/sse), runtime detection
|
|
109
|
+
- **Requirements:** environment variables, API keys, Docker dependencies
|
|
110
|
+
|
|
111
|
+
## API Access
|
|
112
|
+
|
|
113
|
+
Access verified tool schemas programmatically. Create a free API key at [getvet.ai/dashboard](https://getvet.ai/dashboard) โ API Keys.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Fetch tool schemas
|
|
117
|
+
curl -H "x-api-key: vet_sk_YOUR_KEY" https://getvet.ai/api/v1/tools/TOOL_SLUG/schemas
|
|
118
|
+
|
|
119
|
+
# Or use Bearer token
|
|
120
|
+
curl -H "Authorization: Bearer vet_sk_YOUR_KEY" https://getvet.ai/api/v1/tools/TOOL_SLUG/schemas
|
|
121
|
+
|
|
122
|
+
# Bulk fetch (multiple tools at once)
|
|
123
|
+
curl -X POST \
|
|
124
|
+
-H "x-api-key: vet_sk_YOUR_KEY" \
|
|
125
|
+
-H "Content-Type: application/json" \
|
|
126
|
+
-d '{"slugs":["tool-1","tool-2"]}' \
|
|
127
|
+
https://getvet.ai/api/v1/tools/schemas/bulk
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
See [getvet.ai/get-started](https://getvet.ai/get-started) for full documentation.
|
|
120
131
|
|
|
121
|
-
|
|
132
|
+
## Links
|
|
122
133
|
|
|
123
|
-
|
|
134
|
+
- ๐ [getvet.ai](https://getvet.ai) โ Browse the registry
|
|
135
|
+
- ๐ฆ [npm](https://www.npmjs.com/package/@getvetai/cli) โ Package page
|
|
124
136
|
|
|
125
137
|
## License
|
|
126
138
|
|
|
127
|
-
MIT
|
|
139
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getvetai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Security audit CLI for AI skills and MCP servers โ scan, audit, and score tools before you install them",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,11 +14,10 @@
|
|
|
14
14
|
],
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "https://github.com/getvetai/
|
|
18
|
-
"directory": "cli"
|
|
17
|
+
"url": "https://github.com/getvetai/cli.git"
|
|
19
18
|
},
|
|
19
|
+
"bugs": "https://github.com/getvetai/cli/issues",
|
|
20
20
|
"homepage": "https://getvet.ai",
|
|
21
|
-
"bugs": "https://github.com/getvetai/vet/issues",
|
|
22
21
|
"keywords": [
|
|
23
22
|
"ai",
|
|
24
23
|
"security",
|
package/dist/commands/audit.d.ts
DELETED
package/dist/commands/audit.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { resolve } from 'path';
|
|
2
|
-
import ora from 'ora';
|
|
3
|
-
import { discoverTools } from '../utils/config.js';
|
|
4
|
-
import { analyzeSkill } from '../utils/analyzer.js';
|
|
5
|
-
import { analyzeMcp } from '../utils/mcp-analyzer.js';
|
|
6
|
-
import { displayAuditReport } from '../utils/display.js';
|
|
7
|
-
export async function auditCommand(path, options) {
|
|
8
|
-
const pp = resolve(path || '.');
|
|
9
|
-
const spinner = ora(`Scanning ${pp}...`).start();
|
|
10
|
-
try {
|
|
11
|
-
const { tools, sources } = discoverTools(pp);
|
|
12
|
-
if (!tools.length) {
|
|
13
|
-
spinner.info('No AI tools found.');
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
spinner.text = `Found ${tools.length} tools...`;
|
|
17
|
-
const results = [];
|
|
18
|
-
for (const t of tools) {
|
|
19
|
-
spinner.text = `Analyzing ${t.name}...`;
|
|
20
|
-
try {
|
|
21
|
-
if (t.type === 'skill' && t.content)
|
|
22
|
-
results.push(analyzeSkill(t.content, t.name));
|
|
23
|
-
else if (t.type === 'mcp-package' || t.type === 'mcp-config')
|
|
24
|
-
results.push(await analyzeMcp({ npmPackage: t.target }));
|
|
25
|
-
else
|
|
26
|
-
results.push(analyzeSkill('', t.name));
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
results.push({ name: t.name, permissions: [], issues: [{ severity: 'warning', message: 'Analysis failed' }], trustScore: 0, badge: 'unverified', codeQuality: { hasTests: false, hasDocs: false, linesOfCode: 0 }, riskFactors: [] });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
spinner.stop();
|
|
33
|
-
if (options.json)
|
|
34
|
-
console.log(JSON.stringify({ sources, results }, null, 2));
|
|
35
|
-
else
|
|
36
|
-
displayAuditReport(results, sources);
|
|
37
|
-
process.exitCode = options.strict
|
|
38
|
-
? (results.some(r => r.badge === 'unverified' || r.badge === 'flagged') ? 1 : 0)
|
|
39
|
-
: (results.some(r => r.badge === 'flagged') ? 1 : 0);
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
spinner.fail(`Failed: ${err.message}`);
|
|
43
|
-
process.exitCode = 2;
|
|
44
|
-
}
|
|
45
|
-
}
|
package/dist/commands/find.d.ts
DELETED
package/dist/commands/find.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import ora from 'ora';
|
|
2
|
-
import { searchTools } from '../utils/api.js';
|
|
3
|
-
import { displayFindResults } from '../utils/display.js';
|
|
4
|
-
export async function findCommand(query, options) {
|
|
5
|
-
const spinner = ora(`Searching "${query}"...`).start();
|
|
6
|
-
try {
|
|
7
|
-
const items = await searchTools(query);
|
|
8
|
-
const results = items.map((x) => ({
|
|
9
|
-
name: x.name,
|
|
10
|
-
slug: x.slug,
|
|
11
|
-
description: x.description,
|
|
12
|
-
trustScore: x.trustScore ?? x.trust_score,
|
|
13
|
-
badge: x.badge,
|
|
14
|
-
author: x.author,
|
|
15
|
-
version: x.version,
|
|
16
|
-
installs: x.installs,
|
|
17
|
-
}));
|
|
18
|
-
spinner.stop();
|
|
19
|
-
if (options.json)
|
|
20
|
-
console.log(JSON.stringify(results, null, 2));
|
|
21
|
-
else
|
|
22
|
-
displayFindResults(results);
|
|
23
|
-
}
|
|
24
|
-
catch (err) {
|
|
25
|
-
spinner.fail(`Search failed: ${err.message}`);
|
|
26
|
-
process.exitCode = 1;
|
|
27
|
-
}
|
|
28
|
-
}
|
package/dist/commands/install.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { createInterface } from 'readline';
|
|
2
|
-
import { execSync } from 'child_process';
|
|
3
|
-
import ora from 'ora';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { analyzeMcp } from '../utils/mcp-analyzer.js';
|
|
6
|
-
import { displayScanReport } from '../utils/display.js';
|
|
7
|
-
async function confirm(msg) {
|
|
8
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
9
|
-
return new Promise(res => {
|
|
10
|
-
rl.question(msg, a => { rl.close(); res(a.toLowerCase() === 'y' || a.toLowerCase() === 'yes'); });
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
export async function installCommand(pkg, options) {
|
|
14
|
-
const spinner = ora(`Auditing ${pkg}...`).start();
|
|
15
|
-
try {
|
|
16
|
-
const result = await analyzeMcp({ npmPackage: pkg });
|
|
17
|
-
spinner.stop();
|
|
18
|
-
displayScanReport(result);
|
|
19
|
-
if (result.badge === 'flagged') {
|
|
20
|
-
console.log(chalk.red.bold(' โ ๏ธ This package has been flagged!'));
|
|
21
|
-
if (!await confirm(chalk.yellow(' Install anyway? (y/N) '))) {
|
|
22
|
-
console.log(chalk.gray(' Cancelled.'));
|
|
23
|
-
process.exitCode = 1;
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
const s2 = ora(`Installing ${pkg}...`).start();
|
|
28
|
-
try {
|
|
29
|
-
if (options.skill)
|
|
30
|
-
execSync(`npx clawhub install ${pkg}`, { stdio: 'pipe' });
|
|
31
|
-
else
|
|
32
|
-
execSync(`npm install${options.global ? ' -g' : ''} ${pkg}`, { stdio: 'pipe' });
|
|
33
|
-
s2.succeed(`${pkg} installed`);
|
|
34
|
-
}
|
|
35
|
-
catch (e) {
|
|
36
|
-
s2.fail(`Install failed: ${e.message}`);
|
|
37
|
-
process.exitCode = 1;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
spinner.fail(`Audit failed: ${err.message}`);
|
|
42
|
-
process.exitCode = 2;
|
|
43
|
-
}
|
|
44
|
-
}
|
package/dist/commands/scan.d.ts
DELETED
package/dist/commands/scan.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import ora from 'ora';
|
|
4
|
-
import { analyzeSkill } from '../utils/analyzer.js';
|
|
5
|
-
import { analyzeMcp } from '../utils/mcp-analyzer.js';
|
|
6
|
-
import { displayScanReport } from '../utils/display.js';
|
|
7
|
-
import { lookupTool, requestDeepScan } from '../utils/api.js';
|
|
8
|
-
function detect(t) {
|
|
9
|
-
if (/^https?:\/\/github\.com\//.test(t))
|
|
10
|
-
return 'github';
|
|
11
|
-
if (/^https?:\/\//.test(t))
|
|
12
|
-
return 'url';
|
|
13
|
-
if (existsSync(t))
|
|
14
|
-
return 'file';
|
|
15
|
-
return 'npm';
|
|
16
|
-
}
|
|
17
|
-
export async function scanCommand(target, options) {
|
|
18
|
-
const spinner = ora('Analyzing...').start();
|
|
19
|
-
try {
|
|
20
|
-
const tt = detect(target);
|
|
21
|
-
let result;
|
|
22
|
-
let registrySlug;
|
|
23
|
-
// Try registry lookup first for npm packages (unless --offline)
|
|
24
|
-
if (tt === 'npm' && !options.offline) {
|
|
25
|
-
spinner.text = `Checking Vet registry for ${target}...`;
|
|
26
|
-
const registryData = await lookupTool(target);
|
|
27
|
-
if (registryData && registryData.scanTier === 'deep') {
|
|
28
|
-
spinner.succeed(chalk.green('โ Found in Vet registry (deep scan available)'));
|
|
29
|
-
registrySlug = registryData.slug || target;
|
|
30
|
-
result = {
|
|
31
|
-
name: registryData.name || target,
|
|
32
|
-
description: registryData.description,
|
|
33
|
-
type: registryData.type || 'mcp',
|
|
34
|
-
transport: registryData.transport,
|
|
35
|
-
runtime: registryData.runtime,
|
|
36
|
-
tools: registryData.tools || [],
|
|
37
|
-
permissions: registryData.permissions || [],
|
|
38
|
-
issues: registryData.issues || [],
|
|
39
|
-
trustScore: registryData.trustScore ?? registryData.trust_score ?? 50,
|
|
40
|
-
badge: registryData.badge || 'unverified',
|
|
41
|
-
codeQuality: registryData.codeQuality || { hasTests: false, hasDocs: false, linesOfCode: 0 },
|
|
42
|
-
riskFactors: registryData.riskFactors || [],
|
|
43
|
-
overview: registryData.overview,
|
|
44
|
-
envVars: registryData.envVars,
|
|
45
|
-
};
|
|
46
|
-
if (options.json)
|
|
47
|
-
console.log(JSON.stringify(result, null, 2));
|
|
48
|
-
else
|
|
49
|
-
displayScanReport(result, registrySlug);
|
|
50
|
-
process.exitCode = result.badge === 'flagged' ? 1 : 0;
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
if (registryData) {
|
|
54
|
-
registrySlug = registryData.slug || target;
|
|
55
|
-
console.log(chalk.cyan(` โน Found in Vet registry (indexed) โ running local analysis too`));
|
|
56
|
-
console.log(chalk.gray(` View: https://getvet.ai/catalog/${registrySlug}`));
|
|
57
|
-
console.log();
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
// Deep scan request
|
|
61
|
-
if (options.deep && tt === 'npm') {
|
|
62
|
-
spinner.text = `Requesting deep scan for ${target}...`;
|
|
63
|
-
const deepResult = await requestDeepScan(target);
|
|
64
|
-
if (deepResult) {
|
|
65
|
-
spinner.succeed(chalk.green('โ Deep scan requested'));
|
|
66
|
-
if (deepResult.status === 'queued') {
|
|
67
|
-
console.log(chalk.gray(` Deep scan queued. Check back soon at https://getvet.ai/catalog/${target}`));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
spinner.info('Deep scan request failed โ proceeding with local analysis');
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
// Local analysis (current behavior)
|
|
75
|
-
if (tt === 'npm') {
|
|
76
|
-
spinner.text = `Fetching npm: ${target}`;
|
|
77
|
-
result = await analyzeMcp({ npmPackage: target });
|
|
78
|
-
}
|
|
79
|
-
else if (tt === 'github') {
|
|
80
|
-
spinner.text = `Fetching GitHub: ${target}`;
|
|
81
|
-
result = await analyzeMcp({ githubUrl: target });
|
|
82
|
-
}
|
|
83
|
-
else if (tt === 'url') {
|
|
84
|
-
spinner.text = `Fetching: ${target}`;
|
|
85
|
-
const resp = await fetch(target, { headers: { 'User-Agent': 'Vet/1.0' } });
|
|
86
|
-
if (!resp.ok)
|
|
87
|
-
throw new Error(`HTTP ${resp.status}`);
|
|
88
|
-
const content = await resp.text();
|
|
89
|
-
result = target.endsWith('SKILL.md') ? analyzeSkill(content) : await analyzeMcp({ readme: content, name: target.split('/').pop()?.replace(/\.(md|json)$/, '') });
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
const content = readFileSync(target, 'utf-8');
|
|
93
|
-
result = target.endsWith('SKILL.md') ? analyzeSkill(content)
|
|
94
|
-
: (content.includes('MCP') || target.includes('mcp')) ? await analyzeMcp({ readme: content, name: target.split('/').pop()?.replace(/\.(md|json)$/, '') })
|
|
95
|
-
: analyzeSkill(content);
|
|
96
|
-
}
|
|
97
|
-
spinner.stop();
|
|
98
|
-
if (options.json)
|
|
99
|
-
console.log(JSON.stringify(result, null, 2));
|
|
100
|
-
else
|
|
101
|
-
displayScanReport(result, registrySlug);
|
|
102
|
-
process.exitCode = result.badge === 'flagged' ? 1 : 0;
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
spinner.fail(`Failed: ${err.message}`);
|
|
106
|
-
process.exitCode = 2;
|
|
107
|
-
}
|
|
108
|
-
}
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
import { scanCommand } from './commands/scan.js';
|
|
4
|
-
import { auditCommand } from './commands/audit.js';
|
|
5
|
-
import { findCommand } from './commands/find.js';
|
|
6
|
-
import { installCommand } from './commands/install.js';
|
|
7
|
-
const program = new Command();
|
|
8
|
-
program
|
|
9
|
-
.name('vet')
|
|
10
|
-
.description('Security audit CLI for AI skills & MCP servers')
|
|
11
|
-
.version('0.2.0');
|
|
12
|
-
program
|
|
13
|
-
.command('scan')
|
|
14
|
-
.description('Scan a single tool for security issues')
|
|
15
|
-
.argument('<target>', 'URL, npm package, file path, or GitHub repo')
|
|
16
|
-
.option('--json', 'Output JSON instead of formatted report')
|
|
17
|
-
.option('--overview', 'Include AI-generated overview')
|
|
18
|
-
.option('--offline', 'Skip registry lookup')
|
|
19
|
-
.option('--deep', 'Request deep scan from registry')
|
|
20
|
-
.action(scanCommand);
|
|
21
|
-
program
|
|
22
|
-
.command('audit')
|
|
23
|
-
.description('Audit all AI tools in a project')
|
|
24
|
-
.argument('[path]', 'Project path (defaults to current directory)')
|
|
25
|
-
.option('--json', 'Output JSON')
|
|
26
|
-
.option('--strict', 'Exit code 1 if any tool is unverified or flagged')
|
|
27
|
-
.option('--fix', 'Suggest safer alternatives for flagged tools')
|
|
28
|
-
.action(auditCommand);
|
|
29
|
-
program
|
|
30
|
-
.command('find')
|
|
31
|
-
.description('Search for tools by description')
|
|
32
|
-
.argument('<query>', 'Natural language search query')
|
|
33
|
-
.option('--json', 'Output JSON')
|
|
34
|
-
.action(findCommand);
|
|
35
|
-
program
|
|
36
|
-
.command('install')
|
|
37
|
-
.description('Install a package with pre-install security audit')
|
|
38
|
-
.argument('<package>', 'npm package name')
|
|
39
|
-
.option('--json', 'Output JSON')
|
|
40
|
-
.option('-g, --global', 'Install globally')
|
|
41
|
-
.option('--skill', 'Install as OpenClaw skill via clawhub')
|
|
42
|
-
.action(installCommand);
|
|
43
|
-
program.parse();
|
package/dist/types.d.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
export type BadgeType = 'certified' | 'reviewed' | 'unverified' | 'flagged';
|
|
2
|
-
export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
|
|
3
|
-
export type SkillType = 'skill' | 'mcp';
|
|
4
|
-
export type Transport = 'stdio' | 'http' | 'sse';
|
|
5
|
-
export interface Permission {
|
|
6
|
-
type: string;
|
|
7
|
-
target?: string;
|
|
8
|
-
risk: RiskLevel;
|
|
9
|
-
detected_by?: string;
|
|
10
|
-
evidence?: string[];
|
|
11
|
-
}
|
|
12
|
-
export interface SecurityIssue {
|
|
13
|
-
severity: RiskLevel | 'info' | 'warning';
|
|
14
|
-
message: string;
|
|
15
|
-
count?: number;
|
|
16
|
-
evidence?: string[];
|
|
17
|
-
}
|
|
18
|
-
export interface CodeQuality {
|
|
19
|
-
hasTests: boolean;
|
|
20
|
-
hasDocs: boolean;
|
|
21
|
-
hasLicense?: boolean;
|
|
22
|
-
hasPermissionDeclaration?: boolean;
|
|
23
|
-
linesOfCode: number;
|
|
24
|
-
dependencyCount?: number;
|
|
25
|
-
}
|
|
26
|
-
export interface McpTool {
|
|
27
|
-
name: string;
|
|
28
|
-
description?: string;
|
|
29
|
-
inputSchema?: McpToolParam[];
|
|
30
|
-
permissions?: Permission[];
|
|
31
|
-
}
|
|
32
|
-
export interface McpToolParam {
|
|
33
|
-
name: string;
|
|
34
|
-
type: string;
|
|
35
|
-
description: string;
|
|
36
|
-
}
|
|
37
|
-
export interface AnalysisResult {
|
|
38
|
-
name: string;
|
|
39
|
-
description?: string;
|
|
40
|
-
type?: SkillType;
|
|
41
|
-
transport?: Transport;
|
|
42
|
-
runtime?: string;
|
|
43
|
-
tools?: McpTool[];
|
|
44
|
-
permissions: Permission[];
|
|
45
|
-
issues: SecurityIssue[];
|
|
46
|
-
trustScore: number;
|
|
47
|
-
badge: BadgeType;
|
|
48
|
-
codeQuality: CodeQuality;
|
|
49
|
-
riskFactors: string[];
|
|
50
|
-
overview?: string;
|
|
51
|
-
envVars?: string[];
|
|
52
|
-
}
|
package/dist/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/utils/analyzer.d.ts
DELETED
package/dist/utils/analyzer.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
const PERM_PATTERNS = [
|
|
2
|
-
{ pattern: /\bexec\b|\bshell\b|\bcommand\b|\bspawn\b|\bchild_process\b/gi, type: 'exec:shell', risk: 'critical' },
|
|
3
|
-
{ pattern: /\bread\b.*file|\bfs[:\.]read|\bReadFile|\bfile.*read/gi, type: 'fs:read', risk: 'low' },
|
|
4
|
-
{ pattern: /\bwrite\b.*file|\bfs[:\.]write|\bWriteFile|\bfile.*write|\bcreate.*file/gi, type: 'fs:write', risk: 'medium' },
|
|
5
|
-
{ pattern: /\bweb_fetch\b|\bfetch\b|\bhttp[s]?:\/\/|\baxios\b|\brequest\b/gi, type: 'net:outbound', risk: 'medium' },
|
|
6
|
-
{ pattern: /\bbrowser\b|\bpuppeteer\b|\bplaywright\b|\bselenium\b/gi, type: 'browser:control', risk: 'high' },
|
|
7
|
-
{ pattern: /\bmessage\b.*send|\bsend.*message|\bemail\b|\bslack\b|\bdiscord\b|\btelegram\b/gi, type: 'msg:send', risk: 'high' },
|
|
8
|
-
{ pattern: /\bprocess\b|\bbackground\b|\bdaemon\b/gi, type: 'exec:process', risk: 'medium' },
|
|
9
|
-
{ pattern: /\bweb_search\b|\bsearch.*web/gi, type: 'net:search', risk: 'low' },
|
|
10
|
-
{ pattern: /\bcrypto\b|\bsign\b|\bencrypt\b|\bdecrypt\b/gi, type: 'crypto:sign', risk: 'medium' },
|
|
11
|
-
{ pattern: /\bimage\b.*analy|\bvision\b|\bocr\b/gi, type: 'media:analyze', risk: 'low' },
|
|
12
|
-
{ pattern: /\btts\b|\btext.to.speech\b|\bspeech\b/gi, type: 'media:tts', risk: 'low' },
|
|
13
|
-
{ pattern: /\bcanvas\b|\bpresent\b.*ui/gi, type: 'ui:canvas', risk: 'low' },
|
|
14
|
-
{ pattern: /\bnodes?\b.*camera|\bcamera\b.*snap/gi, type: 'device:camera', risk: 'high' },
|
|
15
|
-
{ pattern: /\bscreen\b.*record|\bscreenshot\b/gi, type: 'device:screen', risk: 'medium' },
|
|
16
|
-
{ pattern: /\blocation\b|\bgps\b|\bgeolocat/gi, type: 'device:location', risk: 'high' },
|
|
17
|
-
];
|
|
18
|
-
const RISKY_PATTERNS = [
|
|
19
|
-
{ pattern: /rm\s+-rf\b/g, message: 'Destructive file deletion (rm -rf)', severity: 'critical' },
|
|
20
|
-
{ pattern: /curl\s.*\|\s*bash/g, message: 'Remote code execution (curl | bash)', severity: 'critical' },
|
|
21
|
-
{ pattern: /\beval\s*\(/g, message: 'Dynamic code execution (eval)', severity: 'critical' },
|
|
22
|
-
{ pattern: /\bnew\s+Function\s*\(/g, message: 'Dynamic function creation', severity: 'high' },
|
|
23
|
-
{ pattern: /\b(password|secret|token|api[_-]?key|private[_-]?key|mnemonic)\b/gi, message: 'Credential/secret pattern detected', severity: 'high' },
|
|
24
|
-
{ pattern: /\bchmod\s+[0-7]*7[0-7]*\b/g, message: 'Permissive file permissions', severity: 'high' },
|
|
25
|
-
{ pattern: /\bsudo\b/g, message: 'Sudo/elevated privilege usage', severity: 'critical' },
|
|
26
|
-
{ pattern: /\bssh\b.*connect|\bssh\s+-/g, message: 'SSH connection', severity: 'high' },
|
|
27
|
-
{ pattern: /\bcrontab\b|\bsystemctl\b/g, message: 'System service manipulation', severity: 'high' },
|
|
28
|
-
];
|
|
29
|
-
export function parseSkillName(content, fallback = 'unknown') {
|
|
30
|
-
const h = content.match(/^#\s+(.+)$/m);
|
|
31
|
-
if (h) {
|
|
32
|
-
const n = h[1].trim().replace(/[*_`]/g, '').trim();
|
|
33
|
-
if (n.length > 0 && n.length < 100)
|
|
34
|
-
return n;
|
|
35
|
-
}
|
|
36
|
-
if (fallback !== 'unknown')
|
|
37
|
-
return fallback.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
38
|
-
return fallback;
|
|
39
|
-
}
|
|
40
|
-
export function analyzeSkill(content, name = 'unknown') {
|
|
41
|
-
const parsedName = parseSkillName(content, name);
|
|
42
|
-
const permissions = [], issues = [], seen = new Set();
|
|
43
|
-
for (const p of PERM_PATTERNS) {
|
|
44
|
-
const m = content.match(p.pattern);
|
|
45
|
-
if (m && !seen.has(p.type)) {
|
|
46
|
-
seen.add(p.type);
|
|
47
|
-
permissions.push({ type: p.type, risk: p.risk, detected_by: 'static', evidence: m.slice(0, 3).map(x => x.trim()) });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
for (const r of RISKY_PATTERNS) {
|
|
51
|
-
const m = content.match(r.pattern);
|
|
52
|
-
if (m)
|
|
53
|
-
issues.push({ severity: r.severity, message: r.message, count: m.length, evidence: m.slice(0, 3).map(x => x.trim()) });
|
|
54
|
-
}
|
|
55
|
-
let ts = 80;
|
|
56
|
-
for (const p of permissions) {
|
|
57
|
-
if (p.risk === 'critical')
|
|
58
|
-
ts -= 15;
|
|
59
|
-
else if (p.risk === 'high')
|
|
60
|
-
ts -= 10;
|
|
61
|
-
else if (p.risk === 'medium')
|
|
62
|
-
ts -= 5;
|
|
63
|
-
}
|
|
64
|
-
for (const i of issues) {
|
|
65
|
-
if (i.severity === 'critical')
|
|
66
|
-
ts -= 20;
|
|
67
|
-
else if (i.severity === 'high')
|
|
68
|
-
ts -= 10;
|
|
69
|
-
else if (i.severity === 'medium')
|
|
70
|
-
ts -= 5;
|
|
71
|
-
}
|
|
72
|
-
const cq = {
|
|
73
|
-
hasTests: /\btest\b|\bspec\b|\bjest\b/i.test(content), hasDocs: /\b(readme|documentation|usage|example)\b/i.test(content),
|
|
74
|
-
hasLicense: /\blicense\b|\bMIT\b|\bApache\b/i.test(content), hasPermissionDeclaration: /\bpermission\b|\baccess\b|\brequire\b/i.test(content),
|
|
75
|
-
linesOfCode: content.split('\n').length,
|
|
76
|
-
};
|
|
77
|
-
if (cq.hasTests)
|
|
78
|
-
ts += 5;
|
|
79
|
-
if (cq.hasDocs)
|
|
80
|
-
ts += 3;
|
|
81
|
-
if (cq.hasLicense)
|
|
82
|
-
ts += 2;
|
|
83
|
-
if (cq.hasPermissionDeclaration)
|
|
84
|
-
ts += 5;
|
|
85
|
-
ts = Math.max(0, Math.min(100, ts));
|
|
86
|
-
let badge = 'unverified';
|
|
87
|
-
if (issues.some(i => i.severity === 'critical'))
|
|
88
|
-
badge = 'flagged';
|
|
89
|
-
else if (ts >= 80)
|
|
90
|
-
badge = 'certified';
|
|
91
|
-
else if (ts >= 50)
|
|
92
|
-
badge = 'reviewed';
|
|
93
|
-
return { name: parsedName, type: 'skill', permissions, issues, trustScore: ts, badge, codeQuality: cq, riskFactors: issues.filter(i => i.severity === 'critical' || i.severity === 'high').map(i => i.message) };
|
|
94
|
-
}
|
package/dist/utils/api.d.ts
DELETED
package/dist/utils/api.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
const API_BASE = 'https://getvet.ai';
|
|
2
|
-
export async function lookupTool(slug) {
|
|
3
|
-
try {
|
|
4
|
-
const resp = await fetch(`${API_BASE}/api/skills/${encodeURIComponent(slug)}`, {
|
|
5
|
-
headers: { 'User-Agent': 'vet-cli/0.2.0' },
|
|
6
|
-
signal: AbortSignal.timeout(5000),
|
|
7
|
-
});
|
|
8
|
-
if (resp.ok)
|
|
9
|
-
return await resp.json();
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
export async function searchTools(query) {
|
|
17
|
-
try {
|
|
18
|
-
const resp = await fetch(`${API_BASE}/api/skills/search?q=${encodeURIComponent(query)}`, {
|
|
19
|
-
headers: { 'User-Agent': 'vet-cli/0.2.0' },
|
|
20
|
-
signal: AbortSignal.timeout(5000),
|
|
21
|
-
});
|
|
22
|
-
if (resp.ok) {
|
|
23
|
-
const data = await resp.json();
|
|
24
|
-
return Array.isArray(data) ? data : data.results || data.items || [];
|
|
25
|
-
}
|
|
26
|
-
return [];
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return [];
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
export async function requestDeepScan(slug) {
|
|
33
|
-
try {
|
|
34
|
-
const resp = await fetch(`${API_BASE}/api/tools/${encodeURIComponent(slug)}/deep-scan`, {
|
|
35
|
-
method: 'POST',
|
|
36
|
-
headers: { 'User-Agent': 'vet-cli/0.2.0', 'Content-Type': 'application/json' },
|
|
37
|
-
signal: AbortSignal.timeout(10000),
|
|
38
|
-
});
|
|
39
|
-
if (resp.ok)
|
|
40
|
-
return await resp.json();
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
}
|
package/dist/utils/config.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export interface DiscoveredTool {
|
|
2
|
-
name: string;
|
|
3
|
-
source: string;
|
|
4
|
-
type: 'mcp-config' | 'mcp-package' | 'skill' | 'openclaw-skill';
|
|
5
|
-
target?: string;
|
|
6
|
-
content?: string;
|
|
7
|
-
}
|
|
8
|
-
export declare function discoverTools(projectPath: string): {
|
|
9
|
-
tools: DiscoveredTool[];
|
|
10
|
-
sources: {
|
|
11
|
-
source: string;
|
|
12
|
-
count: number;
|
|
13
|
-
}[];
|
|
14
|
-
};
|
package/dist/utils/config.js
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
export function discoverTools(projectPath) {
|
|
5
|
-
const tools = [], sc = new Map();
|
|
6
|
-
const add = (s, n) => { if (n > 0)
|
|
7
|
-
sc.set(s, (sc.get(s) || 0) + n); };
|
|
8
|
-
const pkgPath = join(projectPath, 'package.json');
|
|
9
|
-
if (existsSync(pkgPath)) {
|
|
10
|
-
try {
|
|
11
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
12
|
-
const all = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
13
|
-
let c = 0;
|
|
14
|
-
for (const d of Object.keys(all)) {
|
|
15
|
-
if (d.includes('mcp') || d.startsWith('@modelcontextprotocol/')) {
|
|
16
|
-
tools.push({ name: d, source: 'package.json', type: 'mcp-package', target: d });
|
|
17
|
-
c++;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
add('package.json', c);
|
|
21
|
-
}
|
|
22
|
-
catch { /* skip */ }
|
|
23
|
-
}
|
|
24
|
-
// Standard MCP config files (mcpServers or servers key at top level)
|
|
25
|
-
const mcpPaths = [
|
|
26
|
-
join(projectPath, '.cursor', 'mcp.json'),
|
|
27
|
-
join(projectPath, 'mcp.json'),
|
|
28
|
-
join(homedir(), '.config', 'claude', 'claude_desktop_config.json'),
|
|
29
|
-
join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
|
|
30
|
-
join(homedir(), '.windsurf', 'mcp.json'),
|
|
31
|
-
join(homedir(), '.config', 'windsurf', 'mcp.json'),
|
|
32
|
-
join(homedir(), '.config', 'cline', 'mcp_settings.json'),
|
|
33
|
-
];
|
|
34
|
-
for (const cp of mcpPaths) {
|
|
35
|
-
if (!existsSync(cp))
|
|
36
|
-
continue;
|
|
37
|
-
try {
|
|
38
|
-
const cfg = JSON.parse(readFileSync(cp, 'utf-8'));
|
|
39
|
-
const svrs = cfg.mcpServers || cfg.servers || {};
|
|
40
|
-
let c = 0;
|
|
41
|
-
for (const [n, v] of Object.entries(svrs)) {
|
|
42
|
-
const target = (v.command === 'npx' ? v.args?.[0] : undefined) || n;
|
|
43
|
-
tools.push({ name: n, source: cp.replace(homedir(), '~'), type: 'mcp-config', target });
|
|
44
|
-
c++;
|
|
45
|
-
}
|
|
46
|
-
add(cp.replace(homedir(), '~'), c);
|
|
47
|
-
}
|
|
48
|
-
catch { /* skip */ }
|
|
49
|
-
}
|
|
50
|
-
// VS Code settings.json โ look for mcp.servers key
|
|
51
|
-
const vscodePath = join(homedir(), '.config', 'Code', 'User', 'settings.json');
|
|
52
|
-
if (existsSync(vscodePath)) {
|
|
53
|
-
try {
|
|
54
|
-
const cfg = JSON.parse(readFileSync(vscodePath, 'utf-8'));
|
|
55
|
-
const svrs = cfg['mcp.servers'] || cfg?.mcp?.servers || {};
|
|
56
|
-
let c = 0;
|
|
57
|
-
for (const [n, v] of Object.entries(svrs)) {
|
|
58
|
-
const target = (v.command === 'npx' ? v.args?.[0] : undefined) || n;
|
|
59
|
-
tools.push({ name: n, source: vscodePath.replace(homedir(), '~'), type: 'mcp-config', target });
|
|
60
|
-
c++;
|
|
61
|
-
}
|
|
62
|
-
add(vscodePath.replace(homedir(), '~'), c);
|
|
63
|
-
}
|
|
64
|
-
catch { /* skip */ }
|
|
65
|
-
}
|
|
66
|
-
// Zed settings.json โ look for mcp key
|
|
67
|
-
const zedPath = join(homedir(), '.config', 'zed', 'settings.json');
|
|
68
|
-
if (existsSync(zedPath)) {
|
|
69
|
-
try {
|
|
70
|
-
const cfg = JSON.parse(readFileSync(zedPath, 'utf-8'));
|
|
71
|
-
const svrs = cfg?.mcp?.servers || cfg?.mcp || {};
|
|
72
|
-
let c = 0;
|
|
73
|
-
for (const [n, v] of Object.entries(svrs)) {
|
|
74
|
-
if (typeof v !== 'object' || v === null)
|
|
75
|
-
continue;
|
|
76
|
-
const target = (v.command === 'npx' ? v.args?.[0] : undefined) || n;
|
|
77
|
-
tools.push({ name: n, source: zedPath.replace(homedir(), '~'), type: 'mcp-config', target });
|
|
78
|
-
c++;
|
|
79
|
-
}
|
|
80
|
-
add(zedPath.replace(homedir(), '~'), c);
|
|
81
|
-
}
|
|
82
|
-
catch { /* skip */ }
|
|
83
|
-
}
|
|
84
|
-
// Continue config.json โ look for mcpServers key
|
|
85
|
-
const continuePath = join(homedir(), '.continue', 'config.json');
|
|
86
|
-
if (existsSync(continuePath)) {
|
|
87
|
-
try {
|
|
88
|
-
const cfg = JSON.parse(readFileSync(continuePath, 'utf-8'));
|
|
89
|
-
const svrs = cfg.mcpServers || {};
|
|
90
|
-
let c = 0;
|
|
91
|
-
for (const [n, v] of Object.entries(svrs)) {
|
|
92
|
-
const target = (v.command === 'npx' ? v.args?.[0] : undefined) || n;
|
|
93
|
-
tools.push({ name: n, source: continuePath.replace(homedir(), '~'), type: 'mcp-config', target });
|
|
94
|
-
c++;
|
|
95
|
-
}
|
|
96
|
-
add(continuePath.replace(homedir(), '~'), c);
|
|
97
|
-
}
|
|
98
|
-
catch { /* skip */ }
|
|
99
|
-
}
|
|
100
|
-
for (const op of [join(projectPath, 'openclaw.json'), join(homedir(), '.openclaw', 'openclaw.json')]) {
|
|
101
|
-
if (!existsSync(op))
|
|
102
|
-
continue;
|
|
103
|
-
try {
|
|
104
|
-
const cfg = JSON.parse(readFileSync(op, 'utf-8'));
|
|
105
|
-
const skills = cfg.skills || cfg.installedSkills || [];
|
|
106
|
-
let c = 0;
|
|
107
|
-
if (Array.isArray(skills)) {
|
|
108
|
-
for (const s of skills) {
|
|
109
|
-
const n = typeof s === 'string' ? s : s.name;
|
|
110
|
-
tools.push({ name: n, source: op.replace(homedir(), '~'), type: 'openclaw-skill', target: n });
|
|
111
|
-
c++;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
add(op.replace(homedir(), '~'), c);
|
|
115
|
-
}
|
|
116
|
-
catch { /* skip */ }
|
|
117
|
-
}
|
|
118
|
-
findSkills(projectPath, tools, sc, 0);
|
|
119
|
-
return { tools, sources: [...sc.entries()].map(([source, count]) => ({ source, count })) };
|
|
120
|
-
}
|
|
121
|
-
function findSkills(dir, tools, sc, depth) {
|
|
122
|
-
if (depth > 4)
|
|
123
|
-
return;
|
|
124
|
-
try {
|
|
125
|
-
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
126
|
-
if (e.name === 'node_modules' || e.name === '.git' || e.name === 'dist')
|
|
127
|
-
continue;
|
|
128
|
-
const fp = join(dir, e.name);
|
|
129
|
-
if (e.isFile() && e.name === 'SKILL.md') {
|
|
130
|
-
const content = readFileSync(fp, 'utf-8');
|
|
131
|
-
const nm = content.match(/^#\s+(.+)$/m)?.[1]?.trim().replace(/[*_`]/g, '') || e.name;
|
|
132
|
-
tools.push({ name: nm, source: fp, type: 'skill', target: fp, content });
|
|
133
|
-
sc.set('SKILL.md files', (sc.get('SKILL.md files') || 0) + 1);
|
|
134
|
-
}
|
|
135
|
-
if (e.isDirectory())
|
|
136
|
-
findSkills(fp, tools, sc, depth + 1);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
catch { /* permission denied */ }
|
|
140
|
-
}
|
package/dist/utils/display.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { AnalysisResult, BadgeType } from '../types.js';
|
|
2
|
-
export declare function displayScanReport(r: AnalysisResult, registrySlug?: string): void;
|
|
3
|
-
export declare function displayAuditReport(results: AnalysisResult[], sources: {
|
|
4
|
-
source: string;
|
|
5
|
-
count: number;
|
|
6
|
-
}[]): void;
|
|
7
|
-
export declare function displayFindResults(results: Array<{
|
|
8
|
-
name: string;
|
|
9
|
-
slug?: string;
|
|
10
|
-
description?: string;
|
|
11
|
-
trustScore?: number;
|
|
12
|
-
badge?: BadgeType;
|
|
13
|
-
author?: string;
|
|
14
|
-
version?: string;
|
|
15
|
-
installs?: number;
|
|
16
|
-
}>): void;
|
package/dist/utils/display.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
const BADGE = {
|
|
3
|
-
certified: { emoji: 'โ
', label: 'Certified', color: chalk.green },
|
|
4
|
-
reviewed: { emoji: '๐', label: 'Reviewed', color: chalk.blue },
|
|
5
|
-
unverified: { emoji: 'โ ๏ธ', label: 'Unverified', color: chalk.yellow },
|
|
6
|
-
flagged: { emoji: '๐ซ', label: 'Flagged', color: chalk.red },
|
|
7
|
-
};
|
|
8
|
-
const RC = { low: chalk.green, medium: chalk.yellow, high: chalk.red, critical: chalk.redBright.bold };
|
|
9
|
-
function bar(score) {
|
|
10
|
-
const f = Math.round(score / 10), e = 10 - f;
|
|
11
|
-
const c = score >= 75 ? chalk.green : score >= 50 ? chalk.yellow : chalk.red;
|
|
12
|
-
return c('โ'.repeat(f)) + chalk.gray('โ'.repeat(e)) + ' ' + c(`${score}/100`);
|
|
13
|
-
}
|
|
14
|
-
function dot(risk) {
|
|
15
|
-
return RC[risk](`โฌค ${risk.charAt(0).toUpperCase() + risk.slice(1)}`);
|
|
16
|
-
}
|
|
17
|
-
function overallRisk(r) {
|
|
18
|
-
if (r.badge === 'flagged')
|
|
19
|
-
return 'critical';
|
|
20
|
-
if (r.trustScore < 50)
|
|
21
|
-
return 'high';
|
|
22
|
-
if (r.trustScore < 75)
|
|
23
|
-
return 'medium';
|
|
24
|
-
return 'low';
|
|
25
|
-
}
|
|
26
|
-
export function displayScanReport(r, registrySlug) {
|
|
27
|
-
const b = BADGE[r.badge], rk = overallRisk(r);
|
|
28
|
-
console.log();
|
|
29
|
-
console.log(chalk.bold(' ๐ Vet Security Report'));
|
|
30
|
-
console.log(chalk.gray(' โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
|
|
31
|
-
console.log();
|
|
32
|
-
console.log(` ${chalk.gray('Name:')} ${chalk.bold(r.name)}`);
|
|
33
|
-
if (r.description)
|
|
34
|
-
console.log(` ${chalk.gray('Description:')} ${r.description.slice(0, 80)}`);
|
|
35
|
-
if (r.type === 'mcp') {
|
|
36
|
-
console.log(` ${chalk.gray('Type:')} MCP Server`);
|
|
37
|
-
if (r.transport)
|
|
38
|
-
console.log(` ${chalk.gray('Transport:')} ${r.transport}`);
|
|
39
|
-
if (r.runtime)
|
|
40
|
-
console.log(` ${chalk.gray('Runtime:')} ${r.runtime}`);
|
|
41
|
-
}
|
|
42
|
-
else
|
|
43
|
-
console.log(` ${chalk.gray('Type:')} AI Skill`);
|
|
44
|
-
console.log();
|
|
45
|
-
console.log(` ${chalk.gray('Trust Score:')} ${bar(r.trustScore)}`);
|
|
46
|
-
console.log(` ${chalk.gray('Badge:')} ${b.emoji} ${b.color(b.label)}`);
|
|
47
|
-
console.log(` ${chalk.gray('Risk:')} ${dot(rk)}`);
|
|
48
|
-
if (r.permissions.length > 0) {
|
|
49
|
-
console.log();
|
|
50
|
-
console.log(chalk.bold(' ๐ Permissions'));
|
|
51
|
-
console.log(chalk.gray(` ${'Permission'.padEnd(25)} ${'Risk'.padEnd(12)} Evidence`));
|
|
52
|
-
console.log(chalk.gray(` ${'โ'.repeat(25)} ${'โ'.repeat(12)} ${'โ'.repeat(30)}`));
|
|
53
|
-
for (const p of r.permissions)
|
|
54
|
-
console.log(` ${p.type.padEnd(25)} ${RC[p.risk](p.risk.padEnd(12))} ${chalk.gray(p.evidence?.slice(0, 2).join(', ') || '')}`);
|
|
55
|
-
}
|
|
56
|
-
if (r.issues.length > 0) {
|
|
57
|
-
console.log();
|
|
58
|
-
console.log(chalk.bold(' โ ๏ธ Issues'));
|
|
59
|
-
for (const i of r.issues) {
|
|
60
|
-
const icon = i.severity === 'critical' ? '๐ด' : i.severity === 'high' ? '๐ ' : i.severity === 'medium' ? '๐ก' : '๐ต';
|
|
61
|
-
const cf = RC[i.severity];
|
|
62
|
-
console.log(` ${icon} ${cf ? cf(i.message) : i.message}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (r.tools && r.tools.length > 0) {
|
|
66
|
-
console.log();
|
|
67
|
-
console.log(chalk.bold(` ๐ง Tools (${r.tools.length})`));
|
|
68
|
-
for (const t of r.tools.slice(0, 15)) {
|
|
69
|
-
console.log(` ${chalk.cyan(t.name.padEnd(25))} ${chalk.gray(t.description?.slice(0, 40) || '')}`);
|
|
70
|
-
const ps = t.inputSchema?.map(p => p.name).join(', ');
|
|
71
|
-
if (ps)
|
|
72
|
-
console.log(` ${' '.repeat(25)} ${chalk.gray(`params: ${ps}`)}`);
|
|
73
|
-
}
|
|
74
|
-
if (r.tools.length > 15)
|
|
75
|
-
console.log(chalk.gray(` ... and ${r.tools.length - 15} more`));
|
|
76
|
-
}
|
|
77
|
-
if (r.envVars && r.envVars.length > 0) {
|
|
78
|
-
console.log();
|
|
79
|
-
console.log(chalk.bold(' ๐ Environment Variables'));
|
|
80
|
-
for (const v of r.envVars)
|
|
81
|
-
console.log(` ${chalk.yellow(v)}`);
|
|
82
|
-
}
|
|
83
|
-
if (r.overview) {
|
|
84
|
-
console.log();
|
|
85
|
-
console.log(chalk.bold(' ๐ Overview'));
|
|
86
|
-
for (const l of r.overview.split('\n'))
|
|
87
|
-
console.log(` ${chalk.gray(l)}`);
|
|
88
|
-
}
|
|
89
|
-
if (registrySlug) {
|
|
90
|
-
console.log();
|
|
91
|
-
console.log(` ${chalk.gray('View full report:')} ${chalk.cyan(`https://getvet.ai/catalog/${registrySlug}`)}`);
|
|
92
|
-
}
|
|
93
|
-
console.log();
|
|
94
|
-
console.log(chalk.gray(' โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
|
|
95
|
-
console.log();
|
|
96
|
-
}
|
|
97
|
-
export function displayAuditReport(results, sources) {
|
|
98
|
-
console.log();
|
|
99
|
-
console.log(chalk.bold(' ๐ Vet Audit Report'));
|
|
100
|
-
console.log(chalk.gray(' โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
|
|
101
|
-
console.log();
|
|
102
|
-
for (const s of sources)
|
|
103
|
-
console.log(` ๐ฆ Found ${s.count} tool${s.count !== 1 ? 's' : ''} in ${s.source}`);
|
|
104
|
-
console.log();
|
|
105
|
-
const W = 35;
|
|
106
|
-
console.log(chalk.gray(` ${'Tool'.padEnd(W)} ${'Score'.padEnd(7)} ${'Badge'.padEnd(16)} Risk`));
|
|
107
|
-
console.log(chalk.gray(` ${'โ'.repeat(W)} ${'โ'.repeat(7)} ${'โ'.repeat(16)} ${'โ'.repeat(12)}`));
|
|
108
|
-
for (const r of results) {
|
|
109
|
-
const b = BADGE[r.badge], rk = overallRisk(r);
|
|
110
|
-
const nm = r.name.length > W - 1 ? r.name.slice(0, W - 4) + '...' : r.name;
|
|
111
|
-
const sc = r.trustScore >= 75 ? chalk.green : r.trustScore >= 50 ? chalk.yellow : chalk.red;
|
|
112
|
-
console.log(` ${nm.padEnd(W)} ${sc(String(r.trustScore).padEnd(7))} ${b.emoji} ${b.color(b.label.padEnd(13))} ${dot(rk)}`);
|
|
113
|
-
}
|
|
114
|
-
console.log();
|
|
115
|
-
console.log(chalk.gray(' โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
|
|
116
|
-
const c = { certified: 0, reviewed: 0, unverified: 0, flagged: 0 };
|
|
117
|
-
for (const r of results)
|
|
118
|
-
c[r.badge]++;
|
|
119
|
-
console.log(` Summary: ${results.length} tools audited | ${c.certified} certified | ${c.reviewed} reviewed | ${c.unverified} unverified | ${c.flagged} flagged`);
|
|
120
|
-
if (c.flagged > 0)
|
|
121
|
-
console.log(chalk.red.bold(` โ ๏ธ ${c.flagged} tool${c.flagged > 1 ? 's' : ''} requires attention`));
|
|
122
|
-
console.log();
|
|
123
|
-
}
|
|
124
|
-
export function displayFindResults(results) {
|
|
125
|
-
if (!results.length) {
|
|
126
|
-
console.log(chalk.yellow('\n No results found.\n'));
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
console.log();
|
|
130
|
-
console.log(chalk.bold(` ๐ Search Results (${results.length})`));
|
|
131
|
-
console.log(chalk.gray(' โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ'));
|
|
132
|
-
for (const r of results) {
|
|
133
|
-
const b = r.badge ? BADGE[r.badge] : BADGE.unverified;
|
|
134
|
-
console.log();
|
|
135
|
-
console.log(` ${b.emoji} ${chalk.bold(r.name)}${r.version ? chalk.gray(` v${r.version}`) : ''}${r.author ? chalk.gray(` by ${r.author}`) : ''}`);
|
|
136
|
-
if (r.description)
|
|
137
|
-
console.log(` ${chalk.gray(r.description.slice(0, 80))}`);
|
|
138
|
-
console.log(` Score: ${r.trustScore != null ? bar(r.trustScore) : chalk.gray('N/A')} Badge: ${b.emoji} ${b.color(b.label)}`);
|
|
139
|
-
if (r.installs != null)
|
|
140
|
-
console.log(` ${chalk.gray(`${r.installs.toLocaleString()} installs`)}`);
|
|
141
|
-
if (r.slug)
|
|
142
|
-
console.log(` ${chalk.cyan(`https://getvet.ai/catalog/${r.slug}`)}`);
|
|
143
|
-
}
|
|
144
|
-
console.log();
|
|
145
|
-
}
|
package/dist/utils/fetch.d.ts
DELETED
package/dist/utils/fetch.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
function detect(content, source) {
|
|
3
|
-
if (/SKILL\.md/i.test(source) || /^#\s+.*skill/im.test(content))
|
|
4
|
-
return 'skill';
|
|
5
|
-
if (/mcp|model.context.protocol/i.test(content))
|
|
6
|
-
return 'mcp';
|
|
7
|
-
if (/^#\s+/m.test(content))
|
|
8
|
-
return 'skill';
|
|
9
|
-
return 'unknown';
|
|
10
|
-
}
|
|
11
|
-
export async function fetchTarget(target) {
|
|
12
|
-
if (existsSync(target)) {
|
|
13
|
-
const c = readFileSync(target, 'utf-8');
|
|
14
|
-
return { content: c, type: detect(c, target), name: target.split('/').pop() || target };
|
|
15
|
-
}
|
|
16
|
-
if (/^https?:\/\//i.test(target)) {
|
|
17
|
-
const r = await fetch(target);
|
|
18
|
-
if (!r.ok)
|
|
19
|
-
throw new Error('HTTP ' + r.status);
|
|
20
|
-
const c = await r.text();
|
|
21
|
-
return { content: c, type: detect(c, target), name: target.split('/').pop()?.replace(/\?.*$/, '') || target };
|
|
22
|
-
}
|
|
23
|
-
if (/^[\w-]+\/[\w.-]+$/.test(target) && !target.startsWith('@')) {
|
|
24
|
-
const r = await fetch('https://raw.githubusercontent.com/' + target + '/main/README.md');
|
|
25
|
-
if (r.ok)
|
|
26
|
-
return { content: await r.text(), type: 'mcp', name: target.split('/')[1] };
|
|
27
|
-
throw new Error('GitHub fetch failed: ' + target);
|
|
28
|
-
}
|
|
29
|
-
if (/^@?[\w-]/.test(target)) {
|
|
30
|
-
const r = await fetch('https://registry.npmjs.org/' + encodeURIComponent(target));
|
|
31
|
-
if (r.ok) {
|
|
32
|
-
const d = await r.json();
|
|
33
|
-
return { content: d.readme || '', type: 'mcp', name: d.name || target };
|
|
34
|
-
}
|
|
35
|
-
throw new Error('npm not found: ' + target);
|
|
36
|
-
}
|
|
37
|
-
throw new Error('Unknown target: ' + target);
|
|
38
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { AnalysisResult } from '../types.js';
|
|
2
|
-
interface PJ {
|
|
3
|
-
name?: string;
|
|
4
|
-
description?: string;
|
|
5
|
-
version?: string;
|
|
6
|
-
dependencies?: Record<string, string>;
|
|
7
|
-
devDependencies?: Record<string, string>;
|
|
8
|
-
engines?: {
|
|
9
|
-
node?: string;
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
export declare function analyzeMcp(input: {
|
|
13
|
-
npmPackage?: string;
|
|
14
|
-
githubUrl?: string;
|
|
15
|
-
readme?: string;
|
|
16
|
-
name?: string;
|
|
17
|
-
packageJson?: PJ;
|
|
18
|
-
}): Promise<AnalysisResult>;
|
|
19
|
-
export {};
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
const TOOL_RULES = [
|
|
2
|
-
{ paramPattern: /\bcommand\b|\bcmd\b|\bshell\b/i, namePattern: /\bexec\b|\brun\b|\bshell\b/i, type: 'exec:shell', risk: 'critical' },
|
|
3
|
-
{ paramPattern: /\burl\b|\bendpoint\b|\bhref\b/i, namePattern: /\bfetch\b|\brequest\b|\bhttp\b/i, type: 'net:outbound', risk: 'medium' },
|
|
4
|
-
{ paramPattern: /\bpath\b|\bfile\b|\bfilename\b|\bdirectory\b/i, namePattern: /\bread\b|\bget\b|\blist\b|\bsearch\b/i, type: 'fs:read', risk: 'medium' },
|
|
5
|
-
{ paramPattern: /\bpath\b|\bfile\b|\bfilename\b/i, namePattern: /\bwrite\b|\bcreate\b|\bsave\b|\bupdate\b|\bedit\b/i, type: 'fs:write', risk: 'medium' },
|
|
6
|
-
{ paramPattern: /\bquery\b|\bsql\b|\bstatement\b/i, namePattern: /\bquery\b|\bsql\b|\bdb\b|\bdatabase\b/i, type: 'db:query', risk: 'medium' },
|
|
7
|
-
{ paramPattern: null, namePattern: /\bemail\b|\bmessage\b|\bsend\b|\bnotif/i, type: 'msg:send', risk: 'medium' },
|
|
8
|
-
{ paramPattern: null, namePattern: /\bdelete\b|\bremove\b|\bdrop\b|\bpurge\b|\bdestroy\b/i, type: 'data:delete', risk: 'high' },
|
|
9
|
-
];
|
|
10
|
-
const RISKY = [
|
|
11
|
-
{ pattern: /rm\s+-rf\b/g, message: 'Destructive file deletion (rm -rf)', severity: 'critical' },
|
|
12
|
-
{ pattern: /curl\s.*\|\s*bash/g, message: 'Remote code execution (curl | bash)', severity: 'critical' },
|
|
13
|
-
{ pattern: /\beval\s*\(/g, message: 'Dynamic code execution (eval)', severity: 'critical' },
|
|
14
|
-
{ pattern: /\bnew\s+Function\s*\(/g, message: 'Dynamic function creation', severity: 'high' },
|
|
15
|
-
{ pattern: /\bchild_process\b|\bspawn\b|\bexecSync\b/g, message: 'Shell execution via child_process', severity: 'high' },
|
|
16
|
-
{ pattern: /\bsudo\b/g, message: 'Sudo/elevated privilege usage', severity: 'critical' },
|
|
17
|
-
{ pattern: /\b(password|secret|token|api[_-]?key|private[_-]?key)\b/gi, message: 'Credential/secret pattern detected', severity: 'medium' },
|
|
18
|
-
];
|
|
19
|
-
const SENS_ENV = /key|token|secret|password|credential|auth/i;
|
|
20
|
-
function detectTransport(t) {
|
|
21
|
-
if (/\bsse\b|server.sent.event/i.test(t))
|
|
22
|
-
return 'sse';
|
|
23
|
-
if (/\bhttp\b.*transport|streamablehttp|\/mcp\b/i.test(t) && !/\bstdio\b/i.test(t))
|
|
24
|
-
return 'http';
|
|
25
|
-
return 'stdio';
|
|
26
|
-
}
|
|
27
|
-
function detectRuntime(pj, t) {
|
|
28
|
-
if (pj?.dependencies?.['@modelcontextprotocol/sdk'] || pj?.devDependencies?.['typescript'] || pj?.engines?.node)
|
|
29
|
-
return 'node';
|
|
30
|
-
if (/pyproject\.toml|requirements\.txt|\bpip\b|\bpython\b/i.test(t))
|
|
31
|
-
return 'python';
|
|
32
|
-
if (/\bgo\.mod\b/i.test(t))
|
|
33
|
-
return 'go';
|
|
34
|
-
if (/\bCargo\.toml\b/i.test(t))
|
|
35
|
-
return 'rust';
|
|
36
|
-
return 'node';
|
|
37
|
-
}
|
|
38
|
-
function parseTools(text) {
|
|
39
|
-
const tools = [];
|
|
40
|
-
for (const m of text.matchAll(/###\s+`?(\w[\w_-]*)`?\s*\n([\s\S]*?)(?=\n###\s|\n##\s|$)/g)) {
|
|
41
|
-
const name = m[1], body = m[2];
|
|
42
|
-
if (/^(installation|setup|configuration|usage|example|prerequisites|requirements|license|contributing)/i.test(name))
|
|
43
|
-
continue;
|
|
44
|
-
const desc = body.split('\n').find((l) => l.trim() && !l.startsWith('#') && !l.startsWith('|') && !l.startsWith('-'))?.trim() || '';
|
|
45
|
-
const params = [];
|
|
46
|
-
for (const r of body.matchAll(/\|\s*`?(\w+)`?\s*\|\s*(\w+)\s*\|\s*([^|]*)\|/g))
|
|
47
|
-
params.push({ name: r[1], type: r[2], description: r[3].trim() });
|
|
48
|
-
for (const b of body.matchAll(/-\s+[`*]*(\w+)[`*]*\s*(?:\((\w+)\))?[:\s-]+(.+)/g)) {
|
|
49
|
-
if (!params.find((p) => p.name === b[1]))
|
|
50
|
-
params.push({ name: b[1], type: b[2] || 'string', description: b[3].trim() });
|
|
51
|
-
}
|
|
52
|
-
if (params.length > 0 || /tool|function|method|action|command/i.test(body.slice(0, 200)))
|
|
53
|
-
tools.push({ name, description: desc.slice(0, 300), inputSchema: params });
|
|
54
|
-
}
|
|
55
|
-
if (tools.length === 0) {
|
|
56
|
-
for (const m of text.matchAll(/-\s+\*\*(\w[\w_-]*)\*\*\s*[-:\u2013]\s*(.+)/g)) {
|
|
57
|
-
if (!/install|setup|config|require|license|example|usage/i.test(m[1]))
|
|
58
|
-
tools.push({ name: m[1], description: m[2].trim().slice(0, 300), inputSchema: [] });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return tools;
|
|
62
|
-
}
|
|
63
|
-
function detectEnvVars(text) {
|
|
64
|
-
const vars = new Set();
|
|
65
|
-
for (const m of text.matchAll(/\b([A-Z][A-Z0-9_]{2,})\b/g)) {
|
|
66
|
-
const v = m[1];
|
|
67
|
-
if (/^(API|AWS|AZURE|GCP|GITHUB|OPENAI|ANTHROPIC|DATABASE|DB_|REDIS|MONGO|POSTGRES|MYSQL|SECRET|TOKEN|KEY|PASSWORD|AUTH|SMTP|SLACK|DISCORD|STRIPE)/.test(v) ||
|
|
68
|
-
/_KEY$|_TOKEN$|_SECRET$|_PASSWORD$|_URL$|_URI$|_HOST$|_PORT$/.test(v))
|
|
69
|
-
vars.add(v);
|
|
70
|
-
}
|
|
71
|
-
return [...vars];
|
|
72
|
-
}
|
|
73
|
-
function toolPerms(tool) {
|
|
74
|
-
const perms = [], seen = new Set();
|
|
75
|
-
for (const r of TOOL_RULES) {
|
|
76
|
-
let hit = false;
|
|
77
|
-
if (r.namePattern?.test(tool.name))
|
|
78
|
-
hit = true;
|
|
79
|
-
if (r.paramPattern && tool.inputSchema?.some((p) => r.paramPattern.test(p.name)))
|
|
80
|
-
hit = true;
|
|
81
|
-
if (hit && !seen.has(r.type)) {
|
|
82
|
-
seen.add(r.type);
|
|
83
|
-
perms.push({ type: r.type, risk: r.risk, detected_by: 'mcp-static', evidence: [tool.name] });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return perms;
|
|
87
|
-
}
|
|
88
|
-
export async function analyzeMcp(input) {
|
|
89
|
-
let text = input.readme || '', pj = input.packageJson || null, pkgName = input.name || input.npmPackage || '', desc = '', deps = 0;
|
|
90
|
-
if (input.npmPackage) {
|
|
91
|
-
try {
|
|
92
|
-
const r = await fetch(`https://registry.npmjs.org/${encodeURIComponent(input.npmPackage)}`);
|
|
93
|
-
if (r.ok) {
|
|
94
|
-
const d = await r.json();
|
|
95
|
-
const lat = d['dist-tags']?.latest;
|
|
96
|
-
const vers = d.versions;
|
|
97
|
-
const lv = lat && vers ? vers[lat] : null;
|
|
98
|
-
pkgName = d.name || input.npmPackage;
|
|
99
|
-
desc = d.description || '';
|
|
100
|
-
text = d.readme || text;
|
|
101
|
-
if (lv) {
|
|
102
|
-
pj = lv;
|
|
103
|
-
deps = Object.keys(lv.dependencies || {}).length + Object.keys(lv.devDependencies || {}).length;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
catch { /* continue */ }
|
|
108
|
-
}
|
|
109
|
-
if (input.githubUrl) {
|
|
110
|
-
const m = input.githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
111
|
-
if (m) {
|
|
112
|
-
const owner = m[1], rn = m[2].replace(/\.git$/, '');
|
|
113
|
-
try {
|
|
114
|
-
const rr = await fetch(`https://raw.githubusercontent.com/${owner}/${rn}/main/README.md`, { headers: { 'User-Agent': 'Vet/1.0' } });
|
|
115
|
-
if (rr.ok)
|
|
116
|
-
text = await rr.text();
|
|
117
|
-
const pr = await fetch(`https://raw.githubusercontent.com/${owner}/${rn}/main/package.json`, { headers: { 'User-Agent': 'Vet/1.0' } });
|
|
118
|
-
if (pr.ok) {
|
|
119
|
-
pj = await pr.json();
|
|
120
|
-
pkgName = pj.name || pkgName || rn;
|
|
121
|
-
desc = desc || pj.description || '';
|
|
122
|
-
deps = Object.keys(pj.dependencies || {}).length + Object.keys(pj.devDependencies || {}).length;
|
|
123
|
-
}
|
|
124
|
-
if (!pkgName)
|
|
125
|
-
pkgName = rn;
|
|
126
|
-
}
|
|
127
|
-
catch { /* continue */ }
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (!pkgName)
|
|
131
|
-
pkgName = input.name || 'unknown-mcp';
|
|
132
|
-
const transport = detectTransport(text), runtime = detectRuntime(pj, text), tools = parseTools(text), envVars = detectEnvVars(text);
|
|
133
|
-
const allPerms = [], seenP = new Set();
|
|
134
|
-
for (const tool of tools) {
|
|
135
|
-
tool.permissions = toolPerms(tool);
|
|
136
|
-
for (const p of tool.permissions) {
|
|
137
|
-
if (!seenP.has(p.type)) {
|
|
138
|
-
seenP.add(p.type);
|
|
139
|
-
allPerms.push(p);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const issues = [];
|
|
144
|
-
for (const r of RISKY) {
|
|
145
|
-
const m = text.match(r.pattern);
|
|
146
|
-
if (m)
|
|
147
|
-
issues.push({ severity: r.severity, message: r.message, count: m.length, evidence: m.slice(0, 3) });
|
|
148
|
-
}
|
|
149
|
-
const sensEnv = envVars.filter(v => SENS_ENV.test(v));
|
|
150
|
-
if (sensEnv.length > 0)
|
|
151
|
-
issues.push({ severity: 'medium', message: `Requires sensitive env vars: ${sensEnv.join(', ')}`, count: sensEnv.length, evidence: sensEnv });
|
|
152
|
-
let ts = 75;
|
|
153
|
-
for (const p of allPerms) {
|
|
154
|
-
if (p.risk === 'critical')
|
|
155
|
-
ts -= 15;
|
|
156
|
-
else if (p.risk === 'high')
|
|
157
|
-
ts -= 10;
|
|
158
|
-
else if (p.risk === 'medium')
|
|
159
|
-
ts -= 5;
|
|
160
|
-
}
|
|
161
|
-
for (const i of issues) {
|
|
162
|
-
if (i.severity === 'critical')
|
|
163
|
-
ts -= 20;
|
|
164
|
-
else if (i.severity === 'high')
|
|
165
|
-
ts -= 10;
|
|
166
|
-
else if (i.severity === 'medium')
|
|
167
|
-
ts -= 3;
|
|
168
|
-
}
|
|
169
|
-
if (tools.length > 0)
|
|
170
|
-
ts += 5;
|
|
171
|
-
if (text.length > 500)
|
|
172
|
-
ts += 3;
|
|
173
|
-
ts = Math.max(0, Math.min(100, ts));
|
|
174
|
-
let badge = 'unverified';
|
|
175
|
-
if (issues.some(i => i.severity === 'critical'))
|
|
176
|
-
badge = 'flagged';
|
|
177
|
-
else if (ts >= 75)
|
|
178
|
-
badge = 'certified';
|
|
179
|
-
else if (ts >= 50)
|
|
180
|
-
badge = 'reviewed';
|
|
181
|
-
return {
|
|
182
|
-
name: pkgName, description: desc || text.split('\n').find(l => l.trim() && !l.startsWith('#'))?.trim()?.slice(0, 300) || '',
|
|
183
|
-
type: 'mcp', transport, runtime, tools, permissions: allPerms, issues, trustScore: ts, badge,
|
|
184
|
-
codeQuality: { hasTests: /\btest\b|\bspec\b/i.test(text), hasDocs: text.length > 300, hasLicense: /\blicense\b|\bMIT\b|\bApache\b/i.test(text), hasPermissionDeclaration: /\bpermission\b|\baccess\b/i.test(text), linesOfCode: text.split('\n').length, dependencyCount: deps },
|
|
185
|
-
riskFactors: issues.filter(i => i.severity === 'critical' || i.severity === 'high').map(i => i.message), envVars,
|
|
186
|
-
};
|
|
187
|
-
}
|