@vibecheckai/cli 3.5.0 → 3.5.2
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/bin/registry.js +214 -237
- package/bin/runners/cli-utils.js +33 -2
- package/bin/runners/context/analyzer.js +52 -1
- package/bin/runners/context/generators/cursor.js +2 -49
- package/bin/runners/context/git-context.js +3 -1
- package/bin/runners/context/team-conventions.js +33 -7
- package/bin/runners/lib/analysis-core.js +25 -5
- package/bin/runners/lib/analyzers.js +431 -481
- package/bin/runners/lib/default-config.js +127 -0
- package/bin/runners/lib/doctor/modules/security.js +3 -1
- package/bin/runners/lib/engine/ast-cache.js +210 -0
- package/bin/runners/lib/engine/auth-extractor.js +211 -0
- package/bin/runners/lib/engine/billing-extractor.js +112 -0
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
- package/bin/runners/lib/engine/env-extractor.js +207 -0
- package/bin/runners/lib/engine/express-extractor.js +208 -0
- package/bin/runners/lib/engine/extractors.js +849 -0
- package/bin/runners/lib/engine/index.js +207 -0
- package/bin/runners/lib/engine/repo-index.js +514 -0
- package/bin/runners/lib/engine/types.js +124 -0
- package/bin/runners/lib/engines/accessibility-engine.js +18 -218
- package/bin/runners/lib/engines/api-consistency-engine.js +30 -335
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +27 -292
- package/bin/runners/lib/engines/empty-catch-engine.js +17 -127
- package/bin/runners/lib/engines/mock-data-engine.js +10 -53
- package/bin/runners/lib/engines/performance-issues-engine.js +36 -176
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +54 -382
- package/bin/runners/lib/engines/type-aware-engine.js +39 -263
- package/bin/runners/lib/engines/vibecheck-engines/index.js +13 -122
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +73 -373
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/entitlements-v2.js +73 -97
- package/bin/runners/lib/error-handler.js +44 -3
- package/bin/runners/lib/error-messages.js +289 -0
- package/bin/runners/lib/evidence-pack.js +7 -1
- package/bin/runners/lib/finding-id.js +69 -0
- package/bin/runners/lib/finding-sorter.js +89 -0
- package/bin/runners/lib/html-proof-report.js +700 -350
- package/bin/runners/lib/missions/plan.js +6 -46
- package/bin/runners/lib/missions/templates.js +0 -232
- package/bin/runners/lib/next-action.js +560 -0
- package/bin/runners/lib/prerequisites.js +149 -0
- package/bin/runners/lib/route-detection.js +137 -68
- package/bin/runners/lib/scan-output.js +91 -76
- package/bin/runners/lib/scan-runner.js +135 -0
- package/bin/runners/lib/schemas/ajv-validator.js +464 -0
- package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
- package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
- package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
- package/bin/runners/lib/schemas/run-request.schema.json +108 -0
- package/bin/runners/lib/schemas/validator.js +27 -0
- package/bin/runners/lib/schemas/verdict.schema.json +140 -0
- package/bin/runners/lib/ship-output-enterprise.js +23 -23
- package/bin/runners/lib/ship-output.js +75 -31
- package/bin/runners/lib/terminal-ui.js +6 -113
- package/bin/runners/lib/truth.js +351 -10
- package/bin/runners/lib/unified-cli-output.js +430 -603
- package/bin/runners/lib/unified-output.js +13 -9
- package/bin/runners/runAIAgent.js +10 -5
- package/bin/runners/runAgent.js +0 -3
- package/bin/runners/runAllowlist.js +389 -0
- package/bin/runners/runApprove.js +0 -33
- package/bin/runners/runAuth.js +73 -45
- package/bin/runners/runCheckpoint.js +51 -11
- package/bin/runners/runClassify.js +85 -21
- package/bin/runners/runContext.js +0 -3
- package/bin/runners/runDoctor.js +41 -28
- package/bin/runners/runEvidencePack.js +362 -0
- package/bin/runners/runFirewall.js +0 -3
- package/bin/runners/runFirewallHook.js +0 -3
- package/bin/runners/runFix.js +66 -76
- package/bin/runners/runGuard.js +18 -411
- package/bin/runners/runInit.js +113 -30
- package/bin/runners/runLabs.js +424 -0
- package/bin/runners/runMcp.js +19 -25
- package/bin/runners/runPolish.js +64 -240
- package/bin/runners/runPromptFirewall.js +12 -5
- package/bin/runners/runProve.js +57 -22
- package/bin/runners/runQuickstart.js +531 -0
- package/bin/runners/runReality.js +59 -68
- package/bin/runners/runReport.js +38 -33
- package/bin/runners/runRuntime.js +8 -5
- package/bin/runners/runScan.js +1413 -190
- package/bin/runners/runShip.js +113 -719
- package/bin/runners/runTruth.js +0 -3
- package/bin/runners/runValidate.js +13 -9
- package/bin/runners/runWatch.js +23 -14
- package/bin/scan.js +6 -1
- package/bin/vibecheck.js +204 -185
- package/mcp-server/deprecation-middleware.js +282 -0
- package/mcp-server/handlers/index.ts +15 -0
- package/mcp-server/handlers/tool-handler.ts +554 -0
- package/mcp-server/index-v1.js +698 -0
- package/mcp-server/index.js +210 -238
- package/mcp-server/lib/cache-wrapper.cjs +383 -0
- package/mcp-server/lib/error-envelope.js +138 -0
- package/mcp-server/lib/executor.ts +499 -0
- package/mcp-server/lib/index.ts +19 -0
- package/mcp-server/lib/rate-limiter.js +166 -0
- package/mcp-server/lib/sandbox.test.ts +519 -0
- package/mcp-server/lib/sandbox.ts +395 -0
- package/mcp-server/lib/types.ts +267 -0
- package/mcp-server/package.json +12 -3
- package/mcp-server/registry/tool-registry.js +794 -0
- package/mcp-server/registry/tools.json +605 -0
- package/mcp-server/registry.test.ts +334 -0
- package/mcp-server/tests/tier-gating.test.js +297 -0
- package/mcp-server/tier-auth.js +378 -45
- package/mcp-server/tools-v3.js +353 -442
- package/mcp-server/tsconfig.json +37 -0
- package/mcp-server/vibecheck-2.0-tools.js +14 -1
- package/package.json +1 -1
- package/bin/runners/lib/agent-firewall/learning/learning-engine.js +0 -849
- package/bin/runners/lib/audit-logger.js +0 -532
- package/bin/runners/lib/authority/authorities/architecture.js +0 -364
- package/bin/runners/lib/authority/authorities/compliance.js +0 -341
- package/bin/runners/lib/authority/authorities/human.js +0 -343
- package/bin/runners/lib/authority/authorities/quality.js +0 -420
- package/bin/runners/lib/authority/authorities/security.js +0 -228
- package/bin/runners/lib/authority/index.js +0 -293
- package/bin/runners/lib/bundle/bundle-intelligence.js +0 -846
- package/bin/runners/lib/cli-charts.js +0 -368
- package/bin/runners/lib/cli-config-display.js +0 -405
- package/bin/runners/lib/cli-demo.js +0 -275
- package/bin/runners/lib/cli-errors.js +0 -438
- package/bin/runners/lib/cli-help-formatter.js +0 -439
- package/bin/runners/lib/cli-interactive-menu.js +0 -509
- package/bin/runners/lib/cli-prompts.js +0 -441
- package/bin/runners/lib/cli-scan-cards.js +0 -362
- package/bin/runners/lib/compliance-reporter.js +0 -710
- package/bin/runners/lib/conductor/index.js +0 -671
- package/bin/runners/lib/easy/README.md +0 -123
- package/bin/runners/lib/easy/index.js +0 -140
- package/bin/runners/lib/easy/interactive-wizard.js +0 -788
- package/bin/runners/lib/easy/one-click-firewall.js +0 -564
- package/bin/runners/lib/easy/zero-config-reality.js +0 -714
- package/bin/runners/lib/engines/async-patterns-engine.js +0 -444
- package/bin/runners/lib/engines/bundle-size-engine.js +0 -433
- package/bin/runners/lib/engines/confidence-scoring.js +0 -276
- package/bin/runners/lib/engines/context-detection.js +0 -264
- package/bin/runners/lib/engines/database-patterns-engine.js +0 -429
- package/bin/runners/lib/engines/duplicate-code-engine.js +0 -354
- package/bin/runners/lib/engines/env-variables-engine.js +0 -458
- package/bin/runners/lib/engines/error-handling-engine.js +0 -437
- package/bin/runners/lib/engines/false-positive-prevention.js +0 -630
- package/bin/runners/lib/engines/framework-adapters/index.js +0 -607
- package/bin/runners/lib/engines/framework-detection.js +0 -508
- package/bin/runners/lib/engines/import-order-engine.js +0 -429
- package/bin/runners/lib/engines/naming-conventions-engine.js +0 -544
- package/bin/runners/lib/engines/noise-reduction-engine.js +0 -452
- package/bin/runners/lib/engines/orchestrator.js +0 -334
- package/bin/runners/lib/engines/react-patterns-engine.js +0 -457
- package/bin/runners/lib/engines/vibecheck-engines/lib/ai-hallucination-engine.js +0 -806
- package/bin/runners/lib/engines/vibecheck-engines/lib/smart-fix-engine.js +0 -577
- package/bin/runners/lib/engines/vibecheck-engines/lib/vibe-score-engine.js +0 -543
- package/bin/runners/lib/engines/vibecheck-engines.js +0 -514
- package/bin/runners/lib/enhanced-features/index.js +0 -305
- package/bin/runners/lib/enhanced-output.js +0 -631
- package/bin/runners/lib/enterprise.js +0 -300
- package/bin/runners/lib/firewall/command-validator.js +0 -351
- package/bin/runners/lib/firewall/config.js +0 -341
- package/bin/runners/lib/firewall/content-validator.js +0 -519
- package/bin/runners/lib/firewall/index.js +0 -101
- package/bin/runners/lib/firewall/path-validator.js +0 -256
- package/bin/runners/lib/intelligence/cross-repo-intelligence.js +0 -817
- package/bin/runners/lib/mcp-utils.js +0 -425
- package/bin/runners/lib/output/index.js +0 -1022
- package/bin/runners/lib/policy-engine.js +0 -652
- package/bin/runners/lib/polish/autofix/accessibility-fixes.js +0 -333
- package/bin/runners/lib/polish/autofix/async-handlers.js +0 -273
- package/bin/runners/lib/polish/autofix/dead-code.js +0 -280
- package/bin/runners/lib/polish/autofix/imports-optimizer.js +0 -344
- package/bin/runners/lib/polish/autofix/index.js +0 -200
- package/bin/runners/lib/polish/autofix/remove-consoles.js +0 -209
- package/bin/runners/lib/polish/autofix/strengthen-types.js +0 -245
- package/bin/runners/lib/polish/backend-checks.js +0 -148
- package/bin/runners/lib/polish/documentation-checks.js +0 -111
- package/bin/runners/lib/polish/frontend-checks.js +0 -168
- package/bin/runners/lib/polish/index.js +0 -71
- package/bin/runners/lib/polish/infrastructure-checks.js +0 -131
- package/bin/runners/lib/polish/library-detection.js +0 -175
- package/bin/runners/lib/polish/performance-checks.js +0 -100
- package/bin/runners/lib/polish/security-checks.js +0 -148
- package/bin/runners/lib/polish/utils.js +0 -203
- package/bin/runners/lib/prompt-builder.js +0 -540
- package/bin/runners/lib/proof-certificate.js +0 -634
- package/bin/runners/lib/reality/accessibility-audit.js +0 -946
- package/bin/runners/lib/reality/api-contract-validator.js +0 -1012
- package/bin/runners/lib/reality/chaos-engineering.js +0 -1084
- package/bin/runners/lib/reality/performance-tracker.js +0 -1077
- package/bin/runners/lib/reality/scenario-generator.js +0 -1404
- package/bin/runners/lib/reality/visual-regression.js +0 -852
- package/bin/runners/lib/reality-profiler.js +0 -717
- package/bin/runners/lib/replay/flight-recorder-viewer.js +0 -1160
- package/bin/runners/lib/review/ai-code-review.js +0 -832
- package/bin/runners/lib/rules/custom-rule-engine.js +0 -985
- package/bin/runners/lib/sbom-generator.js +0 -641
- package/bin/runners/lib/scan-output-enhanced.js +0 -512
- package/bin/runners/lib/security/owasp-scanner.js +0 -939
- package/bin/runners/lib/validators/contract-validator.js +0 -283
- package/bin/runners/lib/validators/dead-export-detector.js +0 -279
- package/bin/runners/lib/validators/dep-audit.js +0 -245
- package/bin/runners/lib/validators/env-validator.js +0 -319
- package/bin/runners/lib/validators/index.js +0 -120
- package/bin/runners/lib/validators/license-checker.js +0 -252
- package/bin/runners/lib/validators/route-validator.js +0 -290
- package/bin/runners/runAuthority.js +0 -528
- package/bin/runners/runConductor.js +0 -772
- package/bin/runners/runContainer.js +0 -366
- package/bin/runners/runEasy.js +0 -410
- package/bin/runners/runIaC.js +0 -372
- package/bin/runners/runVibe.js +0 -791
- package/mcp-server/tools.js +0 -495
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Executor Module
|
|
3
|
+
*
|
|
4
|
+
* Executes CLI commands in a controlled, secure manner with:
|
|
5
|
+
* - Command allowlisting
|
|
6
|
+
* - Argument sanitization
|
|
7
|
+
* - Timeout enforcement
|
|
8
|
+
* - Output capture and parsing
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn, SpawnOptions } from "child_process";
|
|
12
|
+
import type { ExecutorOptions, ExecutorResult, Finding, ToolResult } from "./types";
|
|
13
|
+
import { createHash } from "crypto";
|
|
14
|
+
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
16
|
+
// CONSTANTS
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
18
|
+
|
|
19
|
+
/** Allowed CLI commands (vibecheck subcommands) */
|
|
20
|
+
const ALLOWED_COMMANDS = new Set([
|
|
21
|
+
// FREE - Inspect & Observe
|
|
22
|
+
"init",
|
|
23
|
+
"doctor",
|
|
24
|
+
"watch",
|
|
25
|
+
"scan",
|
|
26
|
+
"report",
|
|
27
|
+
"context",
|
|
28
|
+
"classify",
|
|
29
|
+
"login",
|
|
30
|
+
"logout",
|
|
31
|
+
"whoami",
|
|
32
|
+
"allowlist",
|
|
33
|
+
"evidence-pack",
|
|
34
|
+
"labs",
|
|
35
|
+
// PRO - Fix, Prove & Enforce
|
|
36
|
+
"ship",
|
|
37
|
+
"fix",
|
|
38
|
+
"prove",
|
|
39
|
+
"reality",
|
|
40
|
+
"gate",
|
|
41
|
+
"guard",
|
|
42
|
+
"mcp",
|
|
43
|
+
"checkpoint",
|
|
44
|
+
"approve",
|
|
45
|
+
"polish",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/** Maximum output size (10MB) */
|
|
49
|
+
const MAX_OUTPUT_SIZE = 10 * 1024 * 1024;
|
|
50
|
+
|
|
51
|
+
/** Default timeout (5 minutes) */
|
|
52
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
53
|
+
|
|
54
|
+
/** Environment variables to pass through */
|
|
55
|
+
const PASSTHROUGH_ENV_VARS = [
|
|
56
|
+
"PATH",
|
|
57
|
+
"NODE_ENV",
|
|
58
|
+
"HOME",
|
|
59
|
+
"USERPROFILE",
|
|
60
|
+
"VIBECHECK_API_KEY",
|
|
61
|
+
"VIBECHECK_API_URL",
|
|
62
|
+
"VIBECHECK_DEBUG",
|
|
63
|
+
"DEBUG",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
67
|
+
// EXECUTOR CLASS
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
69
|
+
|
|
70
|
+
export class CliExecutor {
|
|
71
|
+
private readonly cwd: string;
|
|
72
|
+
private readonly timeoutMs: number;
|
|
73
|
+
private readonly env: Record<string, string>;
|
|
74
|
+
private readonly requestId: string;
|
|
75
|
+
private readonly traceId?: string;
|
|
76
|
+
|
|
77
|
+
constructor(options: ExecutorOptions) {
|
|
78
|
+
this.cwd = options.cwd;
|
|
79
|
+
this.timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
80
|
+
this.requestId = options.requestId;
|
|
81
|
+
this.traceId = options.traceId;
|
|
82
|
+
|
|
83
|
+
// Build sanitized environment
|
|
84
|
+
this.env = this.buildEnvironment(options.env);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Execute a vibecheck CLI command
|
|
89
|
+
*/
|
|
90
|
+
async execute(command: string, args: string[]): Promise<ExecutorResult> {
|
|
91
|
+
// Validate command is allowed
|
|
92
|
+
if (!this.isCommandAllowed(command)) {
|
|
93
|
+
return {
|
|
94
|
+
exitCode: 126,
|
|
95
|
+
stdout: "",
|
|
96
|
+
stderr: `Command not allowed: ${command}`,
|
|
97
|
+
timedOut: false,
|
|
98
|
+
durationMs: 0,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Sanitize arguments
|
|
103
|
+
const sanitizedArgs = this.sanitizeArgs(args);
|
|
104
|
+
|
|
105
|
+
// Build full command: npx vibecheck <command> <args> --json
|
|
106
|
+
const fullArgs = [
|
|
107
|
+
"vibecheck",
|
|
108
|
+
command,
|
|
109
|
+
...sanitizedArgs,
|
|
110
|
+
"--json", // Always request JSON output
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const startTime = Date.now();
|
|
114
|
+
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
const spawnOptions: SpawnOptions = {
|
|
117
|
+
cwd: this.cwd,
|
|
118
|
+
env: this.env,
|
|
119
|
+
shell: false, // Security: don't use shell
|
|
120
|
+
timeout: this.timeoutMs,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
let stdout = "";
|
|
124
|
+
let stderr = "";
|
|
125
|
+
let timedOut = false;
|
|
126
|
+
|
|
127
|
+
const proc = spawn("npx", fullArgs, spawnOptions);
|
|
128
|
+
|
|
129
|
+
// Collect stdout with size limit
|
|
130
|
+
proc.stdout?.on("data", (data: Buffer) => {
|
|
131
|
+
if (stdout.length < MAX_OUTPUT_SIZE) {
|
|
132
|
+
stdout += data.toString();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Collect stderr with size limit
|
|
137
|
+
proc.stderr?.on("data", (data: Buffer) => {
|
|
138
|
+
if (stderr.length < MAX_OUTPUT_SIZE) {
|
|
139
|
+
stderr += data.toString();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Handle timeout
|
|
144
|
+
const timeoutHandle = setTimeout(() => {
|
|
145
|
+
timedOut = true;
|
|
146
|
+
proc.kill("SIGTERM");
|
|
147
|
+
setTimeout(() => proc.kill("SIGKILL"), 1000);
|
|
148
|
+
}, this.timeoutMs);
|
|
149
|
+
|
|
150
|
+
// Handle process exit
|
|
151
|
+
proc.on("close", (code) => {
|
|
152
|
+
clearTimeout(timeoutHandle);
|
|
153
|
+
const durationMs = Date.now() - startTime;
|
|
154
|
+
|
|
155
|
+
resolve({
|
|
156
|
+
exitCode: code ?? 1,
|
|
157
|
+
stdout: this.sanitizeOutput(stdout),
|
|
158
|
+
stderr: this.sanitizeOutput(stderr),
|
|
159
|
+
timedOut,
|
|
160
|
+
durationMs,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Handle spawn errors
|
|
165
|
+
proc.on("error", (err) => {
|
|
166
|
+
clearTimeout(timeoutHandle);
|
|
167
|
+
const durationMs = Date.now() - startTime;
|
|
168
|
+
|
|
169
|
+
resolve({
|
|
170
|
+
exitCode: 127,
|
|
171
|
+
stdout: "",
|
|
172
|
+
stderr: `Spawn error: ${err.message}`,
|
|
173
|
+
timedOut: false,
|
|
174
|
+
durationMs,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check if a command is allowed
|
|
182
|
+
*/
|
|
183
|
+
private isCommandAllowed(command: string): boolean {
|
|
184
|
+
return ALLOWED_COMMANDS.has(command);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Sanitize command arguments
|
|
189
|
+
*/
|
|
190
|
+
private sanitizeArgs(args: string[]): string[] {
|
|
191
|
+
return args.map((arg) => {
|
|
192
|
+
// Remove shell metacharacters
|
|
193
|
+
let sanitized = arg.replace(/[;&|`$(){}[\]<>!#*?~]/g, "");
|
|
194
|
+
|
|
195
|
+
// Limit argument length
|
|
196
|
+
if (sanitized.length > 1000) {
|
|
197
|
+
sanitized = sanitized.slice(0, 1000);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return sanitized;
|
|
201
|
+
}).filter(Boolean);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Build sanitized environment
|
|
206
|
+
*/
|
|
207
|
+
private buildEnvironment(extra?: Record<string, string>): Record<string, string> {
|
|
208
|
+
const env: Record<string, string> = {};
|
|
209
|
+
|
|
210
|
+
// Pass through allowed env vars
|
|
211
|
+
for (const key of PASSTHROUGH_ENV_VARS) {
|
|
212
|
+
if (process.env[key]) {
|
|
213
|
+
env[key] = process.env[key]!;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Add tracing headers
|
|
218
|
+
env.VIBECHECK_REQUEST_ID = this.requestId;
|
|
219
|
+
if (this.traceId) {
|
|
220
|
+
env.VIBECHECK_TRACE_ID = this.traceId;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Merge extra env (sanitized)
|
|
224
|
+
if (extra) {
|
|
225
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
226
|
+
// Only allow alphanumeric env var names
|
|
227
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(key)) {
|
|
228
|
+
env[key] = String(value).slice(0, 1000);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return env;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Sanitize command output (redact secrets)
|
|
238
|
+
*/
|
|
239
|
+
private sanitizeOutput(output: string): string {
|
|
240
|
+
// Redact potential secrets
|
|
241
|
+
let sanitized = output;
|
|
242
|
+
|
|
243
|
+
// API keys
|
|
244
|
+
sanitized = sanitized.replace(
|
|
245
|
+
/(api[_-]?key|token|secret|password|auth)[=:]["']?[\w-]{20,}/gi,
|
|
246
|
+
"$1=<REDACTED>"
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// JWT tokens
|
|
250
|
+
sanitized = sanitized.replace(
|
|
251
|
+
/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
|
|
252
|
+
"<JWT_REDACTED>"
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// Truncate if too long
|
|
256
|
+
if (sanitized.length > MAX_OUTPUT_SIZE) {
|
|
257
|
+
sanitized = sanitized.slice(0, MAX_OUTPUT_SIZE) + "\n... (truncated)";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return sanitized;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
265
|
+
// OUTPUT PARSING
|
|
266
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Parse CLI JSON output into canonical ToolResult
|
|
270
|
+
*/
|
|
271
|
+
export function parseCliOutput(stdout: string, stderr: string): ToolResult {
|
|
272
|
+
// Try to parse JSON from stdout
|
|
273
|
+
let parsed: Record<string, unknown> | null = null;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// Find JSON in output (may have other text before/after)
|
|
277
|
+
const jsonMatch = stdout.match(/\{[\s\S]*\}/);
|
|
278
|
+
if (jsonMatch) {
|
|
279
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
// Not valid JSON
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!parsed) {
|
|
286
|
+
return {
|
|
287
|
+
raw: { stdout, stderr },
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Extract verdict
|
|
292
|
+
const verdict = normalizeVerdict(parsed.verdict || parsed.status);
|
|
293
|
+
|
|
294
|
+
// Extract and normalize findings
|
|
295
|
+
const rawFindings = (parsed.findings || parsed.issues || parsed.results || []) as unknown[];
|
|
296
|
+
const findings = normalizeFindings(rawFindings);
|
|
297
|
+
|
|
298
|
+
// Extract summary
|
|
299
|
+
const summary = normalizeSummary(parsed.summary || parsed.stats, findings);
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
verdict,
|
|
303
|
+
findings,
|
|
304
|
+
summary,
|
|
305
|
+
raw: parsed,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Normalize verdict string
|
|
311
|
+
*/
|
|
312
|
+
function normalizeVerdict(input: unknown): ToolResult["verdict"] | undefined {
|
|
313
|
+
if (!input) return undefined;
|
|
314
|
+
|
|
315
|
+
const v = String(input).toUpperCase();
|
|
316
|
+
switch (v) {
|
|
317
|
+
case "SHIP":
|
|
318
|
+
case "PASS":
|
|
319
|
+
case "SUCCESS":
|
|
320
|
+
case "OK":
|
|
321
|
+
return "PASS";
|
|
322
|
+
case "WARN":
|
|
323
|
+
case "WARNING":
|
|
324
|
+
case "WARNINGS":
|
|
325
|
+
return "WARN";
|
|
326
|
+
case "BLOCK":
|
|
327
|
+
case "FAIL":
|
|
328
|
+
case "FAILURE":
|
|
329
|
+
case "ERROR":
|
|
330
|
+
return "BLOCK";
|
|
331
|
+
default:
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Normalize findings array
|
|
338
|
+
*/
|
|
339
|
+
function normalizeFindings(raw: unknown[]): Finding[] {
|
|
340
|
+
if (!Array.isArray(raw)) return [];
|
|
341
|
+
|
|
342
|
+
return raw.map((item) => {
|
|
343
|
+
const f = item as Record<string, unknown>;
|
|
344
|
+
|
|
345
|
+
const ruleId = String(f.ruleId || f.rule_id || f.rule || f.code || "unknown");
|
|
346
|
+
const filePath = String(f.path || f.file || f.filePath || "unknown");
|
|
347
|
+
const line = Number(f.line || f.lineNumber || f.startLine || 1);
|
|
348
|
+
const message = String(f.message || f.msg || f.description || "");
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
id: generateFindingId(ruleId, filePath, line, message),
|
|
352
|
+
ruleId,
|
|
353
|
+
severity: normalizeSeverity(f.severity || f.level),
|
|
354
|
+
message,
|
|
355
|
+
path: filePath,
|
|
356
|
+
line,
|
|
357
|
+
column: f.column ? Number(f.column) : undefined,
|
|
358
|
+
snippet: f.snippet ? String(f.snippet) : undefined,
|
|
359
|
+
category: f.category ? String(f.category) : undefined,
|
|
360
|
+
fix: f.fix ? String(f.fix) : undefined,
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Normalize severity string
|
|
367
|
+
*/
|
|
368
|
+
function normalizeSeverity(input: unknown): Finding["severity"] {
|
|
369
|
+
if (!input) return "medium";
|
|
370
|
+
|
|
371
|
+
const s = String(input).toLowerCase();
|
|
372
|
+
switch (s) {
|
|
373
|
+
case "critical":
|
|
374
|
+
case "crit":
|
|
375
|
+
case "fatal":
|
|
376
|
+
return "critical";
|
|
377
|
+
case "high":
|
|
378
|
+
case "error":
|
|
379
|
+
return "high";
|
|
380
|
+
case "medium":
|
|
381
|
+
case "med":
|
|
382
|
+
case "warning":
|
|
383
|
+
case "warn":
|
|
384
|
+
return "medium";
|
|
385
|
+
case "low":
|
|
386
|
+
case "minor":
|
|
387
|
+
return "low";
|
|
388
|
+
case "info":
|
|
389
|
+
case "information":
|
|
390
|
+
case "hint":
|
|
391
|
+
return "info";
|
|
392
|
+
default:
|
|
393
|
+
return "medium";
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Generate stable finding ID
|
|
399
|
+
*/
|
|
400
|
+
export function generateFindingId(ruleId: string, path: string, line: number, message: string): string {
|
|
401
|
+
const input = `${ruleId}|${path}|${line}|${message}`;
|
|
402
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 16);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Normalize summary
|
|
407
|
+
*/
|
|
408
|
+
function normalizeSummary(raw: unknown, findings: Finding[]): ToolResult["summary"] {
|
|
409
|
+
const summary: ToolResult["summary"] = {
|
|
410
|
+
total: findings.length,
|
|
411
|
+
bySeverity: {},
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// Count by severity
|
|
415
|
+
for (const f of findings) {
|
|
416
|
+
summary.bySeverity[f.severity] = (summary.bySeverity[f.severity] || 0) + 1;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Merge with raw summary if available
|
|
420
|
+
if (raw && typeof raw === "object") {
|
|
421
|
+
const r = raw as Record<string, unknown>;
|
|
422
|
+
if (r.filesScanned) summary.filesScanned = Number(r.filesScanned);
|
|
423
|
+
if (r.scanDurationMs) summary.scanDurationMs = Number(r.scanDurationMs);
|
|
424
|
+
if (r.byCategory) summary.byCategory = r.byCategory as Record<string, number>;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return summary;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Sort findings in stable order:
|
|
432
|
+
* - severity desc (critical > high > medium > low > info)
|
|
433
|
+
* - rule_id asc
|
|
434
|
+
* - path asc
|
|
435
|
+
* - line asc
|
|
436
|
+
*/
|
|
437
|
+
export function sortFindings(findings: Finding[]): Finding[] {
|
|
438
|
+
const severityOrder: Record<string, number> = {
|
|
439
|
+
critical: 0,
|
|
440
|
+
high: 1,
|
|
441
|
+
medium: 2,
|
|
442
|
+
low: 3,
|
|
443
|
+
info: 4,
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
return [...findings].sort((a, b) => {
|
|
447
|
+
// Severity descending (critical first)
|
|
448
|
+
const sevDiff = severityOrder[a.severity] - severityOrder[b.severity];
|
|
449
|
+
if (sevDiff !== 0) return sevDiff;
|
|
450
|
+
|
|
451
|
+
// Rule ID ascending
|
|
452
|
+
const ruleDiff = a.ruleId.localeCompare(b.ruleId);
|
|
453
|
+
if (ruleDiff !== 0) return ruleDiff;
|
|
454
|
+
|
|
455
|
+
// Path ascending
|
|
456
|
+
const pathDiff = a.path.localeCompare(b.path);
|
|
457
|
+
if (pathDiff !== 0) return pathDiff;
|
|
458
|
+
|
|
459
|
+
// Line ascending
|
|
460
|
+
return a.line - b.line;
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
465
|
+
// CONVENIENCE FUNCTIONS
|
|
466
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Build CLI arguments from tool args and arg map
|
|
470
|
+
*/
|
|
471
|
+
export function buildCliArgs(
|
|
472
|
+
toolArgs: Record<string, unknown>,
|
|
473
|
+
argMap: Record<string, string>,
|
|
474
|
+
fixedFlags?: string[]
|
|
475
|
+
): string[] {
|
|
476
|
+
const args: string[] = [];
|
|
477
|
+
|
|
478
|
+
// Add fixed flags
|
|
479
|
+
if (fixedFlags) {
|
|
480
|
+
args.push(...fixedFlags);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Map tool args to CLI flags
|
|
484
|
+
for (const [key, flag] of Object.entries(argMap)) {
|
|
485
|
+
const value = toolArgs[key];
|
|
486
|
+
|
|
487
|
+
if (value === undefined || value === null) continue;
|
|
488
|
+
|
|
489
|
+
if (typeof value === "boolean") {
|
|
490
|
+
if (value) args.push(flag);
|
|
491
|
+
} else if (Array.isArray(value)) {
|
|
492
|
+
args.push(flag, value.join(","));
|
|
493
|
+
} else {
|
|
494
|
+
args.push(flag, String(value));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return args;
|
|
499
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server Library Exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Types
|
|
6
|
+
export * from "./types";
|
|
7
|
+
|
|
8
|
+
// Sandbox
|
|
9
|
+
export { PathSandbox, createSandbox, validatePath, sanitizePath } from "./sandbox";
|
|
10
|
+
export type { SandboxConfig, SandboxResult, SandboxViolationType } from "./sandbox";
|
|
11
|
+
|
|
12
|
+
// Executor
|
|
13
|
+
export {
|
|
14
|
+
CliExecutor,
|
|
15
|
+
parseCliOutput,
|
|
16
|
+
sortFindings,
|
|
17
|
+
buildCliArgs,
|
|
18
|
+
generateFindingId,
|
|
19
|
+
} from "./executor";
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-user rate limiting for MCP tools
|
|
3
|
+
*
|
|
4
|
+
* Enforces rate limits per user (identified by userId/apiKey) rather than
|
|
5
|
+
* per server instance. This prevents users from bypassing limits by creating
|
|
6
|
+
* multiple connections.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TIER_LIMITS = {
|
|
10
|
+
free: { perMinute: 30, perHour: 200 },
|
|
11
|
+
starter: { perMinute: 60, perHour: 500 },
|
|
12
|
+
pro: { perMinute: 120, perHour: 2000 },
|
|
13
|
+
enterprise: { perMinute: 300, perHour: 10000 },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
class PerUserRateLimiter {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.windowMs = 60 * 1000; // 1 minute window
|
|
19
|
+
this.store = new Map(); // userId -> { count, windowStart, hourlyCount, hourlyWindowStart }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a request is allowed for a user
|
|
24
|
+
* @param {string} userId - User identifier (API key hash or user ID)
|
|
25
|
+
* @param {string} tier - User tier (free, pro, etc.)
|
|
26
|
+
* @returns {{ allowed: boolean, retryAfter?: number, limit?: number, remaining?: number }}
|
|
27
|
+
*/
|
|
28
|
+
check(userId, tier) {
|
|
29
|
+
const limits = TIER_LIMITS[tier] || TIER_LIMITS.free;
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const key = userId;
|
|
32
|
+
|
|
33
|
+
let record = this.store.get(key);
|
|
34
|
+
|
|
35
|
+
// Initialize or reset window if expired
|
|
36
|
+
if (!record || now - record.windowStart > this.windowMs) {
|
|
37
|
+
record = {
|
|
38
|
+
count: 0,
|
|
39
|
+
windowStart: now,
|
|
40
|
+
hourlyCount: record?.hourlyCount || 0,
|
|
41
|
+
hourlyWindowStart: record?.hourlyWindowStart || now,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Reset hourly window if expired
|
|
46
|
+
if (now - record.hourlyWindowStart > 60 * 60 * 1000) {
|
|
47
|
+
record.hourlyCount = 0;
|
|
48
|
+
record.hourlyWindowStart = now;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check per-minute limit
|
|
52
|
+
if (record.count >= limits.perMinute) {
|
|
53
|
+
const retryAfter = Math.ceil((record.windowStart + this.windowMs - now) / 1000);
|
|
54
|
+
return {
|
|
55
|
+
allowed: false,
|
|
56
|
+
retryAfter,
|
|
57
|
+
limit: limits.perMinute,
|
|
58
|
+
remaining: 0,
|
|
59
|
+
window: 'minute',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check per-hour limit
|
|
64
|
+
if (limits.perHour && record.hourlyCount >= limits.perHour) {
|
|
65
|
+
const retryAfter = Math.ceil((record.hourlyWindowStart + 60 * 60 * 1000 - now) / 1000);
|
|
66
|
+
return {
|
|
67
|
+
allowed: false,
|
|
68
|
+
retryAfter,
|
|
69
|
+
limit: limits.perHour,
|
|
70
|
+
remaining: 0,
|
|
71
|
+
window: 'hour',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Increment counters
|
|
76
|
+
record.count++;
|
|
77
|
+
if (limits.perHour) {
|
|
78
|
+
record.hourlyCount++;
|
|
79
|
+
}
|
|
80
|
+
this.store.set(key, record);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
allowed: true,
|
|
84
|
+
limit: limits.perMinute,
|
|
85
|
+
remaining: limits.perMinute - record.count,
|
|
86
|
+
hourlyLimit: limits.perHour,
|
|
87
|
+
hourlyRemaining: limits.perHour ? limits.perHour - record.hourlyCount : undefined,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get current rate limit status for a user (without incrementing)
|
|
93
|
+
* @param {string} userId - User identifier
|
|
94
|
+
* @param {string} tier - User tier
|
|
95
|
+
* @returns {{ limit: number, remaining: number, resetAt: Date }}
|
|
96
|
+
*/
|
|
97
|
+
getStatus(userId, tier) {
|
|
98
|
+
const limits = TIER_LIMITS[tier] || TIER_LIMITS.free;
|
|
99
|
+
const record = this.store.get(userId);
|
|
100
|
+
|
|
101
|
+
if (!record) {
|
|
102
|
+
return {
|
|
103
|
+
limit: limits.perMinute,
|
|
104
|
+
remaining: limits.perMinute,
|
|
105
|
+
resetAt: new Date(Date.now() + this.windowMs),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
if (now - record.windowStart > this.windowMs) {
|
|
111
|
+
return {
|
|
112
|
+
limit: limits.perMinute,
|
|
113
|
+
remaining: limits.perMinute,
|
|
114
|
+
resetAt: new Date(now + this.windowMs),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
limit: limits.perMinute,
|
|
120
|
+
remaining: limits.perMinute - record.count,
|
|
121
|
+
resetAt: new Date(record.windowStart + this.windowMs),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Cleanup expired entries periodically
|
|
127
|
+
* Should be called periodically to prevent memory leaks
|
|
128
|
+
*/
|
|
129
|
+
cleanup() {
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
for (const [key, record] of this.store.entries()) {
|
|
132
|
+
// Remove entries that are older than 2 windows (2 minutes)
|
|
133
|
+
if (now - record.windowStart > this.windowMs * 2) {
|
|
134
|
+
this.store.delete(key);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Reset rate limits for a specific user (for testing/admin)
|
|
141
|
+
* @param {string} userId - User identifier
|
|
142
|
+
*/
|
|
143
|
+
reset(userId) {
|
|
144
|
+
this.store.delete(userId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Clear all rate limit data (for testing)
|
|
149
|
+
*/
|
|
150
|
+
clear() {
|
|
151
|
+
this.store.clear();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const rateLimiter = new PerUserRateLimiter();
|
|
156
|
+
|
|
157
|
+
// Cleanup expired entries every 5 minutes
|
|
158
|
+
if (typeof setInterval !== 'undefined') {
|
|
159
|
+
setInterval(() => rateLimiter.cleanup(), 5 * 60 * 1000);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
rateLimiter,
|
|
164
|
+
TIER_LIMITS,
|
|
165
|
+
PerUserRateLimiter,
|
|
166
|
+
};
|