@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.
Files changed (105) hide show
  1. package/README.md +135 -63
  2. package/bin/_deprecations.js +447 -19
  3. package/bin/_router.js +1 -1
  4. package/bin/registry.js +347 -280
  5. package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
  6. package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
  7. package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
  8. package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
  9. package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
  10. package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
  11. package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
  12. package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
  13. package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
  14. package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
  15. package/bin/runners/lib/agent-firewall/index.js +200 -0
  16. package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
  17. package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
  18. package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +622 -0
  19. package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
  20. package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
  21. package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
  22. package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
  23. package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
  24. package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
  25. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +31 -38
  26. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +68 -3
  27. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +4 -2
  28. package/bin/runners/lib/agent-firewall/risk/thresholds.js +5 -4
  29. package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
  30. package/bin/runners/lib/agent-firewall/session/index.js +26 -0
  31. package/bin/runners/lib/artifact-envelope.js +540 -0
  32. package/bin/runners/lib/auth-shared.js +977 -0
  33. package/bin/runners/lib/checkpoint.js +941 -0
  34. package/bin/runners/lib/cleanup/engine.js +571 -0
  35. package/bin/runners/lib/cleanup/index.js +53 -0
  36. package/bin/runners/lib/cleanup/output.js +375 -0
  37. package/bin/runners/lib/cleanup/rules.js +1060 -0
  38. package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
  39. package/bin/runners/lib/doctor/failure-signatures.js +526 -0
  40. package/bin/runners/lib/doctor/fix-script.js +336 -0
  41. package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
  42. package/bin/runners/lib/doctor/modules/index.js +62 -3
  43. package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
  44. package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
  45. package/bin/runners/lib/doctor/safe-repair.js +384 -0
  46. package/bin/runners/lib/engines/attack-detector.js +1192 -0
  47. package/bin/runners/lib/entitlements-v2.js +2 -2
  48. package/bin/runners/lib/error-messages.js +1 -1
  49. package/bin/runners/lib/missions/briefing.js +427 -0
  50. package/bin/runners/lib/missions/checkpoint.js +753 -0
  51. package/bin/runners/lib/missions/hardening.js +851 -0
  52. package/bin/runners/lib/missions/plan.js +421 -32
  53. package/bin/runners/lib/missions/safety-gates.js +645 -0
  54. package/bin/runners/lib/missions/schema.js +478 -0
  55. package/bin/runners/lib/packs/bundle.js +675 -0
  56. package/bin/runners/lib/packs/evidence-pack.js +671 -0
  57. package/bin/runners/lib/packs/pack-factory.js +837 -0
  58. package/bin/runners/lib/packs/permissions-pack.js +686 -0
  59. package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
  60. package/bin/runners/lib/report-output.js +6 -6
  61. package/bin/runners/lib/safelist/index.js +96 -0
  62. package/bin/runners/lib/safelist/integration.js +334 -0
  63. package/bin/runners/lib/safelist/matcher.js +696 -0
  64. package/bin/runners/lib/safelist/schema.js +948 -0
  65. package/bin/runners/lib/safelist/store.js +438 -0
  66. package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
  67. package/bin/runners/lib/ship-gate.js +832 -0
  68. package/bin/runners/lib/ship-manifest.js +1153 -0
  69. package/bin/runners/lib/ship-output.js +1 -1
  70. package/bin/runners/lib/unified-cli-output.js +710 -383
  71. package/bin/runners/lib/upsell.js +3 -3
  72. package/bin/runners/lib/why-tree.js +650 -0
  73. package/bin/runners/runAllowlist.js +33 -4
  74. package/bin/runners/runApprove.js +240 -1122
  75. package/bin/runners/runAudit.js +692 -0
  76. package/bin/runners/runAuth.js +325 -29
  77. package/bin/runners/runCheckpoint.js +442 -494
  78. package/bin/runners/runCleanup.js +343 -0
  79. package/bin/runners/runDoctor.js +269 -19
  80. package/bin/runners/runFix.js +411 -32
  81. package/bin/runners/runForge.js +411 -0
  82. package/bin/runners/runIntent.js +906 -0
  83. package/bin/runners/runKickoff.js +878 -0
  84. package/bin/runners/runLaunch.js +2000 -0
  85. package/bin/runners/runLink.js +785 -0
  86. package/bin/runners/runMcp.js +1741 -837
  87. package/bin/runners/runPacks.js +2089 -0
  88. package/bin/runners/runPolish.js +41 -0
  89. package/bin/runners/runSafelist.js +1190 -0
  90. package/bin/runners/runScan.js +21 -9
  91. package/bin/runners/runShield.js +1282 -0
  92. package/bin/runners/runShip.js +395 -16
  93. package/bin/vibecheck.js +34 -6
  94. package/mcp-server/README.md +117 -158
  95. package/mcp-server/handlers/tool-handler.ts +3 -3
  96. package/mcp-server/index.js +16 -0
  97. package/mcp-server/intent-firewall-interceptor.js +529 -0
  98. package/mcp-server/manifest.json +473 -0
  99. package/mcp-server/package.json +1 -1
  100. package/mcp-server/registry/tool-registry.js +315 -523
  101. package/mcp-server/registry/tools.json +442 -428
  102. package/mcp-server/tier-auth.js +164 -16
  103. package/mcp-server/tools-v3.js +70 -16
  104. package/package.json +1 -1
  105. 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
+ };