@vibecheckai/cli 3.6.1 → 3.8.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 +135 -63
- package/bin/_deprecations.js +447 -19
- package/bin/_router.js +1 -1
- package/bin/registry.js +347 -280
- package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
- package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
- package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
- package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
- package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
- package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
- package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
- package/bin/runners/lib/agent-firewall/index.js +200 -0
- package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
- package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
- package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +622 -0
- package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
- package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
- package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
- package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
- package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
- package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +31 -38
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +68 -3
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +4 -2
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +5 -4
- package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
- package/bin/runners/lib/agent-firewall/session/index.js +26 -0
- package/bin/runners/lib/artifact-envelope.js +540 -0
- package/bin/runners/lib/auth-shared.js +977 -0
- package/bin/runners/lib/checkpoint.js +941 -0
- package/bin/runners/lib/cleanup/engine.js +571 -0
- package/bin/runners/lib/cleanup/index.js +53 -0
- package/bin/runners/lib/cleanup/output.js +375 -0
- package/bin/runners/lib/cleanup/rules.js +1060 -0
- package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
- package/bin/runners/lib/doctor/failure-signatures.js +526 -0
- package/bin/runners/lib/doctor/fix-script.js +336 -0
- package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
- package/bin/runners/lib/doctor/modules/index.js +62 -3
- package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
- package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
- package/bin/runners/lib/doctor/safe-repair.js +384 -0
- package/bin/runners/lib/engines/attack-detector.js +1192 -0
- package/bin/runners/lib/entitlements-v2.js +2 -2
- package/bin/runners/lib/error-messages.js +1 -1
- package/bin/runners/lib/missions/briefing.js +427 -0
- package/bin/runners/lib/missions/checkpoint.js +753 -0
- package/bin/runners/lib/missions/hardening.js +851 -0
- package/bin/runners/lib/missions/plan.js +421 -32
- package/bin/runners/lib/missions/safety-gates.js +645 -0
- package/bin/runners/lib/missions/schema.js +478 -0
- package/bin/runners/lib/packs/bundle.js +675 -0
- package/bin/runners/lib/packs/evidence-pack.js +671 -0
- package/bin/runners/lib/packs/pack-factory.js +837 -0
- package/bin/runners/lib/packs/permissions-pack.js +686 -0
- package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
- package/bin/runners/lib/report-output.js +6 -6
- package/bin/runners/lib/safelist/index.js +96 -0
- package/bin/runners/lib/safelist/integration.js +334 -0
- package/bin/runners/lib/safelist/matcher.js +696 -0
- package/bin/runners/lib/safelist/schema.js +948 -0
- package/bin/runners/lib/safelist/store.js +438 -0
- package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
- package/bin/runners/lib/ship-gate.js +832 -0
- package/bin/runners/lib/ship-manifest.js +1153 -0
- package/bin/runners/lib/ship-output.js +1 -1
- package/bin/runners/lib/unified-cli-output.js +710 -383
- package/bin/runners/lib/upsell.js +3 -3
- package/bin/runners/lib/why-tree.js +650 -0
- package/bin/runners/runAllowlist.js +33 -4
- package/bin/runners/runApprove.js +240 -1122
- package/bin/runners/runAudit.js +692 -0
- package/bin/runners/runAuth.js +325 -29
- package/bin/runners/runCheckpoint.js +442 -494
- package/bin/runners/runCleanup.js +343 -0
- package/bin/runners/runDoctor.js +269 -19
- package/bin/runners/runFix.js +411 -32
- package/bin/runners/runForge.js +411 -0
- package/bin/runners/runIntent.js +906 -0
- package/bin/runners/runKickoff.js +878 -0
- package/bin/runners/runLaunch.js +2000 -0
- package/bin/runners/runLink.js +785 -0
- package/bin/runners/runMcp.js +1741 -837
- package/bin/runners/runPacks.js +2089 -0
- package/bin/runners/runPolish.js +41 -0
- package/bin/runners/runSafelist.js +1190 -0
- package/bin/runners/runScan.js +21 -9
- package/bin/runners/runShield.js +1282 -0
- package/bin/runners/runShip.js +395 -16
- package/bin/vibecheck.js +34 -6
- package/mcp-server/README.md +117 -158
- package/mcp-server/handlers/tool-handler.ts +3 -3
- package/mcp-server/index.js +16 -0
- package/mcp-server/intent-firewall-interceptor.js +529 -0
- package/mcp-server/manifest.json +473 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/registry/tool-registry.js +315 -523
- package/mcp-server/registry/tools.json +442 -428
- package/mcp-server/tier-auth.js +164 -16
- package/mcp-server/tools-v3.js +70 -16
- package/package.json +1 -1
- package/bin/runners/runProof.zip +0 -0
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runAudit.js - "Convincing Wrongness" Detector
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* AUDIT: Find what makes the app LOOK done but NOT work.
|
|
6
|
+
* The most dangerous bugs are the invisible ones.
|
|
7
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Ranked "Attack List" with confidence and blast radius
|
|
11
|
+
* - Every finding: file/line evidence + "how to prove" + "how to fix"
|
|
12
|
+
* - --fast mode: cheap signals, single-file analysis
|
|
13
|
+
* - --deep mode: runtime + cross-file analysis
|
|
14
|
+
* - Zero false-positive obsession: show confidence + evidence
|
|
15
|
+
*
|
|
16
|
+
* @version 2.0.0
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
"use strict";
|
|
20
|
+
|
|
21
|
+
const path = require("path");
|
|
22
|
+
const fs = require("fs");
|
|
23
|
+
const { parseGlobalFlags, shouldShowBanner } = require("./lib/global-flags");
|
|
24
|
+
const { EXIT } = require("./lib/exit-codes");
|
|
25
|
+
const { withErrorHandling } = require("./lib/error-handler");
|
|
26
|
+
|
|
27
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
28
|
+
// TERMINAL STYLING
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
30
|
+
|
|
31
|
+
const SUPPORTS_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
32
|
+
const SUPPORTS_UNICODE = process.platform !== "win32" || process.env.WT_SESSION || process.env.TERM_PROGRAM;
|
|
33
|
+
|
|
34
|
+
const c = SUPPORTS_COLOR ? {
|
|
35
|
+
reset: "\x1b[0m",
|
|
36
|
+
bold: "\x1b[1m",
|
|
37
|
+
dim: "\x1b[2m",
|
|
38
|
+
italic: "\x1b[3m",
|
|
39
|
+
underline: "\x1b[4m",
|
|
40
|
+
red: "\x1b[31m",
|
|
41
|
+
green: "\x1b[32m",
|
|
42
|
+
yellow: "\x1b[33m",
|
|
43
|
+
blue: "\x1b[34m",
|
|
44
|
+
magenta: "\x1b[35m",
|
|
45
|
+
cyan: "\x1b[36m",
|
|
46
|
+
white: "\x1b[37m",
|
|
47
|
+
gray: "\x1b[90m",
|
|
48
|
+
brightRed: "\x1b[91m",
|
|
49
|
+
brightGreen: "\x1b[92m",
|
|
50
|
+
brightYellow: "\x1b[93m",
|
|
51
|
+
bgRed: "\x1b[41m",
|
|
52
|
+
bgYellow: "\x1b[43m",
|
|
53
|
+
bgGreen: "\x1b[42m",
|
|
54
|
+
bgMagenta: "\x1b[45m",
|
|
55
|
+
} : Object.fromEntries([
|
|
56
|
+
"reset", "bold", "dim", "italic", "underline",
|
|
57
|
+
"red", "green", "yellow", "blue", "magenta", "cyan", "white", "gray",
|
|
58
|
+
"brightRed", "brightGreen", "brightYellow",
|
|
59
|
+
"bgRed", "bgYellow", "bgGreen", "bgMagenta"
|
|
60
|
+
].map(k => [k, ""]));
|
|
61
|
+
|
|
62
|
+
const sym = SUPPORTS_UNICODE ? {
|
|
63
|
+
skull: "💀",
|
|
64
|
+
fire: "🔥",
|
|
65
|
+
warning: "⚠️",
|
|
66
|
+
bomb: "💣",
|
|
67
|
+
ghost: "👻",
|
|
68
|
+
mask: "🎭",
|
|
69
|
+
lock: "🔒",
|
|
70
|
+
unlock: "🔓",
|
|
71
|
+
money: "💰",
|
|
72
|
+
check: "✓",
|
|
73
|
+
cross: "✗",
|
|
74
|
+
bullet: "•",
|
|
75
|
+
arrow: "→",
|
|
76
|
+
arrowRight: "▸",
|
|
77
|
+
box: "■",
|
|
78
|
+
boxEmpty: "□",
|
|
79
|
+
line: "─",
|
|
80
|
+
corner: "└",
|
|
81
|
+
tee: "├",
|
|
82
|
+
vertical: "│",
|
|
83
|
+
topLeft: "╭",
|
|
84
|
+
topRight: "╮",
|
|
85
|
+
bottomLeft: "╰",
|
|
86
|
+
bottomRight: "╯",
|
|
87
|
+
star: "★",
|
|
88
|
+
target: "◎",
|
|
89
|
+
} : {
|
|
90
|
+
skull: "[X]",
|
|
91
|
+
fire: "(!)",
|
|
92
|
+
warning: "[!]",
|
|
93
|
+
bomb: "[*]",
|
|
94
|
+
ghost: "[G]",
|
|
95
|
+
mask: "[M]",
|
|
96
|
+
lock: "[L]",
|
|
97
|
+
unlock: "[U]",
|
|
98
|
+
money: "[$]",
|
|
99
|
+
check: "[v]",
|
|
100
|
+
cross: "[x]",
|
|
101
|
+
bullet: "*",
|
|
102
|
+
arrow: "->",
|
|
103
|
+
arrowRight: ">",
|
|
104
|
+
box: "#",
|
|
105
|
+
boxEmpty: "o",
|
|
106
|
+
line: "-",
|
|
107
|
+
corner: "+",
|
|
108
|
+
tee: "+",
|
|
109
|
+
vertical: "|",
|
|
110
|
+
topLeft: "+",
|
|
111
|
+
topRight: "+",
|
|
112
|
+
bottomLeft: "+",
|
|
113
|
+
bottomRight: "+",
|
|
114
|
+
star: "*",
|
|
115
|
+
target: "@",
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
119
|
+
// UNIFIED CLI OUTPUT
|
|
120
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
121
|
+
|
|
122
|
+
let _cliOutput = null;
|
|
123
|
+
function getCliOutput() {
|
|
124
|
+
if (!_cliOutput) _cliOutput = require("./lib/unified-cli-output");
|
|
125
|
+
return _cliOutput;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
129
|
+
// BANNER
|
|
130
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
131
|
+
|
|
132
|
+
const BANNER_FALLBACK = `
|
|
133
|
+
${c.bgRed}${c.bold} ${c.reset}
|
|
134
|
+
${c.bgRed}${c.bold} ${sym.skull} AUDIT - Convincing Wrongness Detector ${c.reset}
|
|
135
|
+
${c.bgRed}${c.bold} ${c.reset}
|
|
136
|
+
|
|
137
|
+
${c.dim}Find code that LOOKS done but DOESN'T work.${c.reset}
|
|
138
|
+
${c.dim}The most dangerous bugs are the invisible ones.${c.reset}
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
function printBanner(projectRoot = process.cwd()) {
|
|
142
|
+
try {
|
|
143
|
+
const cli = getCliOutput();
|
|
144
|
+
console.log(cli.renderFullHeader("audit", {
|
|
145
|
+
version: "2.0.0",
|
|
146
|
+
tier: "FREE",
|
|
147
|
+
integrity: "ANALYZING",
|
|
148
|
+
target: projectRoot,
|
|
149
|
+
sessionId: cli.generateSessionId(),
|
|
150
|
+
}));
|
|
151
|
+
} catch {
|
|
152
|
+
console.log(BANNER_FALLBACK);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
157
|
+
// ARGUMENT PARSING
|
|
158
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
159
|
+
|
|
160
|
+
function parseArgs(args) {
|
|
161
|
+
const { flags: globalFlags, cleanArgs } = parseGlobalFlags(args);
|
|
162
|
+
|
|
163
|
+
const opts = {
|
|
164
|
+
help: globalFlags.help || false,
|
|
165
|
+
json: globalFlags.json || false,
|
|
166
|
+
ci: globalFlags.ci || false,
|
|
167
|
+
quiet: globalFlags.quiet || false,
|
|
168
|
+
verbose: globalFlags.verbose || false,
|
|
169
|
+
noBanner: globalFlags.noBanner || false,
|
|
170
|
+
path: globalFlags.path || process.cwd(),
|
|
171
|
+
output: globalFlags.output || null,
|
|
172
|
+
|
|
173
|
+
// Audit-specific options
|
|
174
|
+
mode: "fast", // "fast" | "deep"
|
|
175
|
+
threshold: "all", // "critical" | "high" | "medium" | "all"
|
|
176
|
+
format: "terminal", // "terminal" | "json" | "sarif" | "markdown"
|
|
177
|
+
maxFindings: 50,
|
|
178
|
+
failOn: null, // "critical" | "high" | "medium" - exit code 1 if found
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
for (let i = 0; i < cleanArgs.length; i++) {
|
|
182
|
+
const arg = cleanArgs[i];
|
|
183
|
+
|
|
184
|
+
switch (arg) {
|
|
185
|
+
case "--fast":
|
|
186
|
+
opts.mode = "fast";
|
|
187
|
+
break;
|
|
188
|
+
case "--deep":
|
|
189
|
+
opts.mode = "deep";
|
|
190
|
+
break;
|
|
191
|
+
case "--threshold":
|
|
192
|
+
opts.threshold = cleanArgs[++i] || "all";
|
|
193
|
+
break;
|
|
194
|
+
case "--format":
|
|
195
|
+
opts.format = cleanArgs[++i] || "terminal";
|
|
196
|
+
break;
|
|
197
|
+
case "--max":
|
|
198
|
+
case "--max-findings":
|
|
199
|
+
opts.maxFindings = parseInt(cleanArgs[++i], 10) || 50;
|
|
200
|
+
break;
|
|
201
|
+
case "--fail-on":
|
|
202
|
+
opts.failOn = cleanArgs[++i] || null;
|
|
203
|
+
break;
|
|
204
|
+
case "--sarif":
|
|
205
|
+
opts.format = "sarif";
|
|
206
|
+
break;
|
|
207
|
+
case "--markdown":
|
|
208
|
+
case "--md":
|
|
209
|
+
opts.format = "markdown";
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return opts;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
218
|
+
// HELP
|
|
219
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
220
|
+
|
|
221
|
+
function printHelp(showBanner = true, projectRoot = process.cwd()) {
|
|
222
|
+
if (showBanner) printBanner(projectRoot);
|
|
223
|
+
|
|
224
|
+
console.log(`
|
|
225
|
+
${c.bold}USAGE${c.reset}
|
|
226
|
+
vibecheck audit [options]
|
|
227
|
+
|
|
228
|
+
${c.bold}DESCRIPTION${c.reset}
|
|
229
|
+
Find code that LOOKS done but DOESN'T work. Detects:
|
|
230
|
+
|
|
231
|
+
${c.red}${sym.skull} DEAD_ROUTE${c.reset} Routes that exist but don't work
|
|
232
|
+
${c.yellow}${sym.ghost} GHOST_ENV${c.reset} Env vars used but never declared
|
|
233
|
+
${c.magenta}${sym.mask} FAKE_SUCCESS${c.reset} UI shows success without operation
|
|
234
|
+
${c.red}${sym.lock} AUTH_DRIFT${c.reset} Auth patterns that look secure but aren't
|
|
235
|
+
${c.yellow}${sym.bomb} MOCK_LANDMINE${c.reset} Mock/TODO code in production paths
|
|
236
|
+
${c.yellow}${sym.warning} SILENT_FAIL${c.reset} Errors swallowed without feedback
|
|
237
|
+
${c.yellow}${sym.fire} OPTIMISTIC_BOMB${c.reset} Optimistic UI without rollback
|
|
238
|
+
${c.green}${sym.money} PAID_THEATER${c.reset} Paid features that aren't enforced
|
|
239
|
+
|
|
240
|
+
${c.bold}OPTIONS${c.reset}
|
|
241
|
+
${c.cyan}--fast${c.reset} Quick scan - cheap signals only (default)
|
|
242
|
+
${c.cyan}--deep${c.reset} Deep scan - runtime + cross-file analysis
|
|
243
|
+
${c.cyan}--threshold${c.reset} <lvl> Show: "critical", "high", "medium", or "all"
|
|
244
|
+
${c.cyan}--fail-on${c.reset} <lvl> Exit 1 if findings at level: "critical", "high"
|
|
245
|
+
${c.cyan}--format${c.reset} <fmt> Output: "terminal", "json", "sarif", "markdown"
|
|
246
|
+
${c.cyan}--sarif${c.reset} Output as SARIF (for GitHub)
|
|
247
|
+
${c.cyan}--max${c.reset} <n> Max findings to display (default: 50)
|
|
248
|
+
${c.cyan}--output${c.reset} <file> Write output to file
|
|
249
|
+
${c.cyan}--json${c.reset} JSON output
|
|
250
|
+
${c.cyan}--quiet${c.reset} Minimal output
|
|
251
|
+
${c.cyan}--ci${c.reset} CI mode (quiet + no-banner + json)
|
|
252
|
+
|
|
253
|
+
${c.bold}EXAMPLES${c.reset}
|
|
254
|
+
${c.dim}# Quick scan${c.reset}
|
|
255
|
+
vibecheck audit
|
|
256
|
+
|
|
257
|
+
${c.dim}# Deep scan with CI integration${c.reset}
|
|
258
|
+
vibecheck audit --deep --fail-on critical --sarif > audit.sarif
|
|
259
|
+
|
|
260
|
+
${c.dim}# Only show critical and high findings${c.reset}
|
|
261
|
+
vibecheck audit --threshold high
|
|
262
|
+
|
|
263
|
+
${c.dim}# Generate markdown report${c.reset}
|
|
264
|
+
vibecheck audit --markdown --output AUDIT.md
|
|
265
|
+
|
|
266
|
+
${c.bold}EXIT CODES${c.reset}
|
|
267
|
+
0 No findings (or below --fail-on threshold)
|
|
268
|
+
1 Findings at or above --fail-on threshold
|
|
269
|
+
2 Error during scan
|
|
270
|
+
`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
274
|
+
// TERMINAL OUTPUT FORMATTERS
|
|
275
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
276
|
+
|
|
277
|
+
function getSeverityStyle(severity) {
|
|
278
|
+
switch (severity) {
|
|
279
|
+
case "critical": return { color: c.bgRed + c.white, icon: sym.skull, label: "CRIT" };
|
|
280
|
+
case "high": return { color: c.red, icon: sym.fire, label: "HIGH" };
|
|
281
|
+
case "medium": return { color: c.yellow, icon: sym.warning, label: "MED " };
|
|
282
|
+
case "low": return { color: c.dim, icon: sym.bullet, label: "LOW " };
|
|
283
|
+
default: return { color: c.dim, icon: sym.bullet, label: "INFO" };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function getTypeIcon(type) {
|
|
288
|
+
switch (type) {
|
|
289
|
+
case "DEAD_ROUTE": return sym.skull;
|
|
290
|
+
case "GHOST_ENV": return sym.ghost;
|
|
291
|
+
case "FAKE_SUCCESS": return sym.mask;
|
|
292
|
+
case "AUTH_DRIFT": return sym.lock;
|
|
293
|
+
case "MOCK_LANDMINE": return sym.bomb;
|
|
294
|
+
case "SILENT_FAIL": return sym.warning;
|
|
295
|
+
case "OPTIMISTIC_BOMB": return sym.fire;
|
|
296
|
+
case "PAID_THEATER": return sym.money;
|
|
297
|
+
default: return sym.bullet;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function formatFinding(finding, index, verbose = false) {
|
|
302
|
+
const sev = getSeverityStyle(finding.severity);
|
|
303
|
+
const typeIcon = getTypeIcon(finding.type);
|
|
304
|
+
|
|
305
|
+
let output = `
|
|
306
|
+
${c.dim}${sym.line.repeat(70)}${c.reset}
|
|
307
|
+
${sev.color}${c.bold} ${sev.label} ${c.reset} ${c.bold}#${index + 1}${c.reset} ${typeIcon} ${c.bold}${finding.title}${c.reset}
|
|
308
|
+
${c.dim}${sym.line.repeat(70)}${c.reset}
|
|
309
|
+
|
|
310
|
+
${c.cyan}${sym.arrowRight} File:${c.reset} ${finding.file}:${finding.line}
|
|
311
|
+
${c.cyan}${sym.arrowRight} Type:${c.reset} ${finding.type}
|
|
312
|
+
${c.cyan}${sym.arrowRight} Confidence:${c.reset} ${finding.confidence}% ${c.dim}Blast Radius: ${finding.blastRadius}/10${c.reset}
|
|
313
|
+
|
|
314
|
+
${c.dim}${sym.vertical}${c.reset} ${c.gray}${finding.snippet}${c.reset}
|
|
315
|
+
|
|
316
|
+
${c.green}${sym.check} Why it looks right:${c.reset}
|
|
317
|
+
${finding.whyConvincing}
|
|
318
|
+
|
|
319
|
+
${c.red}${sym.cross} Why it's wrong:${c.reset}
|
|
320
|
+
${finding.whyWrong}
|
|
321
|
+
|
|
322
|
+
${c.yellow}${sym.target} How to prove:${c.reset}
|
|
323
|
+
${finding.howToProve}
|
|
324
|
+
|
|
325
|
+
${c.cyan}${sym.star} How to fix:${c.reset}
|
|
326
|
+
${finding.howToFix}
|
|
327
|
+
`;
|
|
328
|
+
|
|
329
|
+
if (verbose && finding.attackVector) {
|
|
330
|
+
output += `
|
|
331
|
+
${c.magenta}${sym.bomb} Attack vector:${c.reset}
|
|
332
|
+
${finding.attackVector}
|
|
333
|
+
`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return output;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function formatSummary(report) {
|
|
340
|
+
const { summary, attackScore, manifest, elapsedMs } = report;
|
|
341
|
+
|
|
342
|
+
// Attack score visualization
|
|
343
|
+
const scoreBar = (score) => {
|
|
344
|
+
const filled = Math.round(score / 5);
|
|
345
|
+
const empty = 20 - filled;
|
|
346
|
+
const color = score >= 70 ? c.red : score >= 40 ? c.yellow : c.green;
|
|
347
|
+
return color + sym.box.repeat(filled) + c.dim + sym.boxEmpty.repeat(empty) + c.reset;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
let output = `
|
|
351
|
+
${c.bold}${c.bgMagenta} ${c.reset}
|
|
352
|
+
${c.bold}${c.bgMagenta} ${sym.target} ATTACK SURFACE ANALYSIS ${c.reset}
|
|
353
|
+
${c.bold}${c.bgMagenta} ${c.reset}
|
|
354
|
+
|
|
355
|
+
${c.bold}Attack Score:${c.reset} ${attackScore}/100 ${scoreBar(attackScore)}
|
|
356
|
+
${attackScore >= 70 ? c.red + sym.skull + " HIGH RISK - Critical issues found" + c.reset :
|
|
357
|
+
attackScore >= 40 ? c.yellow + sym.warning + " MEDIUM RISK - Issues need attention" + c.reset :
|
|
358
|
+
c.green + sym.check + " LOW RISK - Looking good!" + c.reset}
|
|
359
|
+
|
|
360
|
+
${c.bold}Findings by Severity:${c.reset}
|
|
361
|
+
${c.bgRed}${c.white} CRITICAL ${c.reset} ${summary.critical}
|
|
362
|
+
${c.red} HIGH ${c.reset} ${summary.high}
|
|
363
|
+
${c.yellow} MEDIUM ${c.reset} ${summary.medium}
|
|
364
|
+
${c.dim} LOW ${c.reset} ${summary.low}
|
|
365
|
+
|
|
366
|
+
${c.bold}Findings by Type:${c.reset}
|
|
367
|
+
`;
|
|
368
|
+
|
|
369
|
+
const typeOrder = [
|
|
370
|
+
"AUTH_DRIFT", "DEAD_ROUTE", "MOCK_LANDMINE", "GHOST_ENV",
|
|
371
|
+
"FAKE_SUCCESS", "SILENT_FAIL", "OPTIMISTIC_BOMB", "PAID_THEATER"
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
for (const type of typeOrder) {
|
|
375
|
+
const count = summary.byType[type] || 0;
|
|
376
|
+
if (count > 0) {
|
|
377
|
+
output += ` ${getTypeIcon(type)} ${type.padEnd(16)} ${count}\n`;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
output += `
|
|
382
|
+
${c.dim}${sym.line.repeat(50)}${c.reset}
|
|
383
|
+
${c.dim}Total: ${summary.total} findings | Critical paths: ${summary.inCriticalPaths}${c.reset}
|
|
384
|
+
${c.dim}Scan time: ${elapsedMs}ms | Manifest: ${manifest.hash.slice(0, 12)}...${c.reset}
|
|
385
|
+
`;
|
|
386
|
+
|
|
387
|
+
return output;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function formatTerminal(report, opts) {
|
|
391
|
+
let output = "";
|
|
392
|
+
|
|
393
|
+
// Filter by threshold
|
|
394
|
+
const thresholdOrder = { critical: 0, high: 1, medium: 2, low: 3, all: 4 };
|
|
395
|
+
const thresholdLevel = thresholdOrder[opts.threshold] || 4;
|
|
396
|
+
|
|
397
|
+
const filteredFindings = report.findings.filter(f => {
|
|
398
|
+
const sevLevel = thresholdOrder[f.severity];
|
|
399
|
+
return sevLevel <= thresholdLevel;
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Show summary first
|
|
403
|
+
output += formatSummary(report);
|
|
404
|
+
|
|
405
|
+
if (filteredFindings.length === 0) {
|
|
406
|
+
output += `\n${c.green}${sym.check} No findings at threshold '${opts.threshold}' or above.${c.reset}\n`;
|
|
407
|
+
return output;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
output += `\n${c.bold}${c.bgRed} ${c.reset}`;
|
|
411
|
+
output += `\n${c.bold}${c.bgRed} ${sym.skull} ATTACK LIST - Ranked by Severity ${c.reset}`;
|
|
412
|
+
output += `\n${c.bold}${c.bgRed} ${c.reset}\n`;
|
|
413
|
+
|
|
414
|
+
// Show findings (up to max)
|
|
415
|
+
const displayFindings = filteredFindings.slice(0, opts.maxFindings);
|
|
416
|
+
|
|
417
|
+
for (let i = 0; i < displayFindings.length; i++) {
|
|
418
|
+
output += formatFinding(displayFindings[i], i, opts.verbose);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (filteredFindings.length > opts.maxFindings) {
|
|
422
|
+
output += `\n${c.dim}... and ${filteredFindings.length - opts.maxFindings} more findings. Use --max to see more.${c.reset}\n`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Action items
|
|
426
|
+
output += `
|
|
427
|
+
${c.bold}${c.cyan}${sym.star} NEXT STEPS${c.reset}
|
|
428
|
+
|
|
429
|
+
1. ${c.bold}Fix critical issues first${c.reset} - these can break your app
|
|
430
|
+
2. ${c.bold}Add to safelist${c.reset} if false positive: ${c.dim}vibecheck safelist add --id <ID>${c.reset}
|
|
431
|
+
3. ${c.bold}Run with --deep${c.reset} for cross-file analysis
|
|
432
|
+
4. ${c.bold}CI integration:${c.reset} vibecheck audit --fail-on critical --sarif
|
|
433
|
+
|
|
434
|
+
${c.dim}Need help? Run: vibecheck fix --id <FINDING_ID>${c.reset}
|
|
435
|
+
`;
|
|
436
|
+
|
|
437
|
+
return output;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
441
|
+
// SARIF OUTPUT (GitHub compatible)
|
|
442
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
443
|
+
|
|
444
|
+
function formatSarif(report) {
|
|
445
|
+
const sarifSeverity = {
|
|
446
|
+
critical: "error",
|
|
447
|
+
high: "error",
|
|
448
|
+
medium: "warning",
|
|
449
|
+
low: "note",
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const sarifLevel = {
|
|
453
|
+
critical: "error",
|
|
454
|
+
high: "error",
|
|
455
|
+
medium: "warning",
|
|
456
|
+
low: "note",
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
461
|
+
version: "2.1.0",
|
|
462
|
+
runs: [{
|
|
463
|
+
tool: {
|
|
464
|
+
driver: {
|
|
465
|
+
name: "vibecheck-audit",
|
|
466
|
+
version: report.version,
|
|
467
|
+
informationUri: "https://docs.vibecheckai.dev/audit",
|
|
468
|
+
rules: [
|
|
469
|
+
{ id: "DEAD_ROUTE", shortDescription: { text: "Dead route - endpoint doesn't exist" } },
|
|
470
|
+
{ id: "GHOST_ENV", shortDescription: { text: "Missing environment variable" } },
|
|
471
|
+
{ id: "FAKE_SUCCESS", shortDescription: { text: "Fake success - no actual operation" } },
|
|
472
|
+
{ id: "AUTH_DRIFT", shortDescription: { text: "Authentication vulnerability" } },
|
|
473
|
+
{ id: "MOCK_LANDMINE", shortDescription: { text: "Mock/TODO code in production" } },
|
|
474
|
+
{ id: "SILENT_FAIL", shortDescription: { text: "Silent error handling" } },
|
|
475
|
+
{ id: "OPTIMISTIC_BOMB", shortDescription: { text: "Optimistic update without rollback" } },
|
|
476
|
+
{ id: "PAID_THEATER", shortDescription: { text: "Paid feature not enforced" } },
|
|
477
|
+
],
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
results: report.findings.map(f => ({
|
|
481
|
+
ruleId: f.type,
|
|
482
|
+
level: sarifLevel[f.severity],
|
|
483
|
+
message: {
|
|
484
|
+
text: `${f.title}\n\nWhy it looks right: ${f.whyConvincing}\nWhy it's wrong: ${f.whyWrong}\nHow to prove: ${f.howToProve}\nHow to fix: ${f.howToFix}`,
|
|
485
|
+
},
|
|
486
|
+
locations: [{
|
|
487
|
+
physicalLocation: {
|
|
488
|
+
artifactLocation: {
|
|
489
|
+
uri: f.file,
|
|
490
|
+
},
|
|
491
|
+
region: {
|
|
492
|
+
startLine: f.line,
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
}],
|
|
496
|
+
properties: {
|
|
497
|
+
confidence: f.confidence,
|
|
498
|
+
blastRadius: f.blastRadius,
|
|
499
|
+
attackVector: f.attackVector,
|
|
500
|
+
},
|
|
501
|
+
})),
|
|
502
|
+
}],
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
507
|
+
// MARKDOWN OUTPUT
|
|
508
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
509
|
+
|
|
510
|
+
function formatMarkdown(report) {
|
|
511
|
+
const { summary, attackScore, findings, manifest } = report;
|
|
512
|
+
|
|
513
|
+
let md = `# 🔍 AUDIT Report - Convincing Wrongness Detector
|
|
514
|
+
|
|
515
|
+
> Generated: ${report.timestamp}
|
|
516
|
+
> Mode: ${report.mode}
|
|
517
|
+
> Attack Score: **${attackScore}/100**
|
|
518
|
+
|
|
519
|
+
## Summary
|
|
520
|
+
|
|
521
|
+
| Severity | Count |
|
|
522
|
+
|----------|-------|
|
|
523
|
+
| 🔴 Critical | ${summary.critical} |
|
|
524
|
+
| 🟠 High | ${summary.high} |
|
|
525
|
+
| 🟡 Medium | ${summary.medium} |
|
|
526
|
+
| ⚪ Low | ${summary.low} |
|
|
527
|
+
| **Total** | **${summary.total}** |
|
|
528
|
+
|
|
529
|
+
## Findings by Type
|
|
530
|
+
|
|
531
|
+
| Type | Count | Description |
|
|
532
|
+
|------|-------|-------------|
|
|
533
|
+
| DEAD_ROUTE | ${summary.byType.DEAD_ROUTE || 0} | Routes that exist but don't work |
|
|
534
|
+
| GHOST_ENV | ${summary.byType.GHOST_ENV || 0} | Env vars used but never declared |
|
|
535
|
+
| FAKE_SUCCESS | ${summary.byType.FAKE_SUCCESS || 0} | UI shows success without operation |
|
|
536
|
+
| AUTH_DRIFT | ${summary.byType.AUTH_DRIFT || 0} | Auth patterns that aren't secure |
|
|
537
|
+
| MOCK_LANDMINE | ${summary.byType.MOCK_LANDMINE || 0} | Mock/TODO in production |
|
|
538
|
+
| SILENT_FAIL | ${summary.byType.SILENT_FAIL || 0} | Errors swallowed silently |
|
|
539
|
+
| OPTIMISTIC_BOMB | ${summary.byType.OPTIMISTIC_BOMB || 0} | Optimistic UI without rollback |
|
|
540
|
+
| PAID_THEATER | ${summary.byType.PAID_THEATER || 0} | Paid features not enforced |
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## Attack List
|
|
545
|
+
|
|
546
|
+
`;
|
|
547
|
+
|
|
548
|
+
for (let i = 0; i < findings.length; i++) {
|
|
549
|
+
const f = findings[i];
|
|
550
|
+
const sevEmoji = f.severity === "critical" ? "🔴" :
|
|
551
|
+
f.severity === "high" ? "🟠" :
|
|
552
|
+
f.severity === "medium" ? "🟡" : "⚪";
|
|
553
|
+
|
|
554
|
+
md += `### ${sevEmoji} #${i + 1}: ${f.title}
|
|
555
|
+
|
|
556
|
+
- **ID:** \`${f.id}\`
|
|
557
|
+
- **File:** \`${f.file}:${f.line}\`
|
|
558
|
+
- **Type:** ${f.type}
|
|
559
|
+
- **Severity:** ${f.severity}
|
|
560
|
+
- **Confidence:** ${f.confidence}%
|
|
561
|
+
- **Blast Radius:** ${f.blastRadius}/10
|
|
562
|
+
|
|
563
|
+
\`\`\`
|
|
564
|
+
${f.snippet}
|
|
565
|
+
\`\`\`
|
|
566
|
+
|
|
567
|
+
| | |
|
|
568
|
+
|---|---|
|
|
569
|
+
| ✅ **Why it looks right** | ${f.whyConvincing} |
|
|
570
|
+
| ❌ **Why it's wrong** | ${f.whyWrong} |
|
|
571
|
+
| 🎯 **How to prove** | ${f.howToProve} |
|
|
572
|
+
| ⭐ **How to fix** | ${f.howToFix} |
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
md += `
|
|
580
|
+
## Manifest
|
|
581
|
+
|
|
582
|
+
\`\`\`json
|
|
583
|
+
${JSON.stringify(manifest, null, 2)}
|
|
584
|
+
\`\`\`
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
*Report generated by [vibecheck audit](https://docs.vibecheckai.dev/audit)*
|
|
589
|
+
`;
|
|
590
|
+
|
|
591
|
+
return md;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
595
|
+
// MAIN RUNNER
|
|
596
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
597
|
+
|
|
598
|
+
async function runAudit(args, context = {}) {
|
|
599
|
+
const opts = parseArgs(args);
|
|
600
|
+
|
|
601
|
+
if (opts.help) {
|
|
602
|
+
printHelp(shouldShowBanner(opts));
|
|
603
|
+
return EXIT.SUCCESS;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Determine project path
|
|
607
|
+
const projectPath = path.resolve(opts.path);
|
|
608
|
+
|
|
609
|
+
if (!fs.existsSync(projectPath)) {
|
|
610
|
+
console.error(`${c.red}${sym.cross}${c.reset} Path not found: ${projectPath}`);
|
|
611
|
+
return EXIT.NOT_FOUND;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Show banner unless suppressed
|
|
615
|
+
if (!opts.quiet && !opts.json && !opts.ci && !opts.noBanner && opts.format === "terminal") {
|
|
616
|
+
printBanner(projectPath);
|
|
617
|
+
console.log(`${c.dim}Mode: ${opts.mode}${c.reset}\n`);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Run the detector
|
|
621
|
+
const { AttackDetector } = require("./lib/engines/attack-detector");
|
|
622
|
+
const detector = new AttackDetector(projectPath, {
|
|
623
|
+
mode: opts.mode,
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
let report;
|
|
627
|
+
try {
|
|
628
|
+
report = await detector.scan();
|
|
629
|
+
} catch (err) {
|
|
630
|
+
console.error(`${c.red}${sym.cross}${c.reset} Scan failed: ${err.message}`);
|
|
631
|
+
if (opts.verbose) console.error(err.stack);
|
|
632
|
+
return EXIT.INTERNAL_ERROR;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Format output
|
|
636
|
+
let output;
|
|
637
|
+
switch (opts.format) {
|
|
638
|
+
case "json":
|
|
639
|
+
output = JSON.stringify(report, null, 2);
|
|
640
|
+
break;
|
|
641
|
+
case "sarif":
|
|
642
|
+
output = JSON.stringify(formatSarif(report), null, 2);
|
|
643
|
+
break;
|
|
644
|
+
case "markdown":
|
|
645
|
+
output = formatMarkdown(report);
|
|
646
|
+
break;
|
|
647
|
+
default:
|
|
648
|
+
output = formatTerminal(report, opts);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Write output
|
|
652
|
+
if (opts.output) {
|
|
653
|
+
fs.writeFileSync(opts.output, output);
|
|
654
|
+
if (!opts.quiet) {
|
|
655
|
+
console.log(`${c.green}${sym.check}${c.reset} Report written to: ${opts.output}`);
|
|
656
|
+
}
|
|
657
|
+
} else if (opts.format === "terminal") {
|
|
658
|
+
console.log(output);
|
|
659
|
+
} else {
|
|
660
|
+
// JSON/SARIF/Markdown to stdout
|
|
661
|
+
console.log(output);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Determine exit code based on --fail-on
|
|
665
|
+
if (opts.failOn) {
|
|
666
|
+
const thresholdOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
667
|
+
const failLevel = thresholdOrder[opts.failOn];
|
|
668
|
+
|
|
669
|
+
if (failLevel !== undefined) {
|
|
670
|
+
const hasFailure = report.findings.some(f =>
|
|
671
|
+
thresholdOrder[f.severity] <= failLevel
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
if (hasFailure) {
|
|
675
|
+
if (!opts.quiet && opts.format === "terminal") {
|
|
676
|
+
console.log(`\n${c.red}${sym.cross}${c.reset} Failing due to findings at '${opts.failOn}' level or above.`);
|
|
677
|
+
}
|
|
678
|
+
return EXIT.BLOCKING;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return EXIT.SUCCESS;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
687
|
+
// EXPORTS
|
|
688
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
689
|
+
|
|
690
|
+
module.exports = {
|
|
691
|
+
runAudit: withErrorHandling(runAudit, "Audit failed"),
|
|
692
|
+
};
|