@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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # @getvetai/cli
2
2
 
3
- Security audit CLI for AI skills and MCP servers. Scan, audit, and score tools before you install them.
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) โ€” 12,000+ AI tools cataloged and scored
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
- ## What's New in v0.2.0
13
+ Or run without installing:
14
14
 
15
- - **Registry integration** โ€” instant results for known tools from the getvet.ai catalog
16
- - **Expanded MCP config discovery** โ€” Claude, Cursor, VS Code, Windsurf, Cline, Zed, Continue, OpenClaw
17
- - **`--offline` flag** โ€” skip registry lookup for air-gapped environments
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 single tool for security issues. Accepts file paths, URLs, npm packages, or GitHub repos.
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
- # Skip registry, local analysis only
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 SKILL.md
40
- vet scan ./my-skill/SKILL.md
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
- # Output JSON
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. Discovers tools from:
47
+ Audit all AI tools in a project. Auto-discovers MCP configurations from:
54
48
 
55
- - `package.json` (MCP dependencies)
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 code 1 if any tool is unverified or flagged
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 "file management"
87
- vet find "database query tool"
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. Shows the security report and asks for confirmation if the tool is flagged.
86
+ Install a package with a pre-install security audit.
96
87
 
97
88
  ```bash
98
- # Audit + install npm package
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
- ## Trust Scores
96
+ ## Verification Levels
109
97
 
110
- | Score | Badge | Meaning |
98
+ | Level | Badge | Meaning |
111
99
  |-------|-------|---------|
112
- | 75+ | โœ… Certified | No critical issues, good practices |
113
- | 50-74 | ๐Ÿ” Reviewed | Some concerns, use with caution |
114
- | 25-49 | โš ๏ธ Unverified | Not yet reviewed or limited info |
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 read/write, network access, browser control, message sending, device access (camera, screen, location), database queries, crypto operations.
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
- **Security Issues:** destructive commands (`rm -rf`), remote code execution (`curl | bash`), dynamic code eval, credential patterns, elevated privileges (`sudo`), permissive file permissions.
132
+ ## Links
122
133
 
123
- **MCP-specific:** tool parameter analysis, transport detection (stdio/http/sse), runtime detection, environment variable scanning.
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 โ€” [getvet.ai](https://getvet.ai)
139
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getvetai/cli",
3
- "version": "0.2.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/vet.git",
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",
@@ -1,5 +0,0 @@
1
- export declare function auditCommand(path: string | undefined, options: {
2
- json?: boolean;
3
- strict?: boolean;
4
- fix?: boolean;
5
- }): Promise<void>;
@@ -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
- }
@@ -1,3 +0,0 @@
1
- export declare function findCommand(query: string, options: {
2
- json?: boolean;
3
- }): Promise<void>;
@@ -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
- }
@@ -1,5 +0,0 @@
1
- export declare function installCommand(pkg: string, options: {
2
- json?: boolean;
3
- global?: boolean;
4
- skill?: boolean;
5
- }): Promise<void>;
@@ -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
- }
@@ -1,6 +0,0 @@
1
- export declare function scanCommand(target: string, options: {
2
- json?: boolean;
3
- overview?: boolean;
4
- offline?: boolean;
5
- deep?: boolean;
6
- }): Promise<void>;
@@ -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
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
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 {};
@@ -1,3 +0,0 @@
1
- import type { AnalysisResult } from '../types.js';
2
- export declare function parseSkillName(content: string, fallback?: string): string;
3
- export declare function analyzeSkill(content: string, name?: string): AnalysisResult;
@@ -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
- }
@@ -1,3 +0,0 @@
1
- export declare function lookupTool(slug: string): Promise<any | null>;
2
- export declare function searchTools(query: string): Promise<any[]>;
3
- export declare function requestDeepScan(slug: string): Promise<any | null>;
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
- }
@@ -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
- };
@@ -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
- }
@@ -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;
@@ -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
- }
@@ -1,6 +0,0 @@
1
- export interface FetchedContent {
2
- content: string;
3
- type: 'skill' | 'mcp' | 'unknown';
4
- name: string;
5
- }
6
- export declare function fetchTarget(target: string): Promise<FetchedContent>;
@@ -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
- }