@vibecheckai/cli 3.1.2 → 3.1.4
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 +60 -33
- package/bin/registry.js +319 -34
- package/bin/runners/CLI_REFACTOR_SUMMARY.md +229 -0
- package/bin/runners/REPORT_AUDIT.md +64 -0
- package/bin/runners/lib/entitlements-v2.js +97 -28
- package/bin/runners/lib/entitlements.js +3 -6
- package/bin/runners/lib/init-wizard.js +1 -1
- package/bin/runners/lib/report-engine.js +459 -280
- package/bin/runners/lib/report-html.js +1154 -1423
- package/bin/runners/lib/report-output.js +187 -0
- package/bin/runners/lib/report-templates.js +848 -850
- package/bin/runners/lib/scan-output.js +545 -0
- package/bin/runners/lib/server-usage.js +0 -12
- package/bin/runners/lib/ship-output.js +641 -0
- package/bin/runners/lib/status-output.js +253 -0
- package/bin/runners/lib/terminal-ui.js +853 -0
- package/bin/runners/runCheckpoint.js +502 -0
- package/bin/runners/runContracts.js +105 -0
- package/bin/runners/runExport.js +93 -0
- package/bin/runners/runFix.js +31 -24
- package/bin/runners/runInit.js +377 -112
- package/bin/runners/runInstall.js +1 -5
- package/bin/runners/runLabs.js +3 -3
- package/bin/runners/runPolish.js +2452 -0
- package/bin/runners/runProve.js +2 -2
- package/bin/runners/runReport.js +251 -200
- package/bin/runners/runRuntime.js +110 -0
- package/bin/runners/runScan.js +477 -379
- package/bin/runners/runSecurity.js +92 -0
- package/bin/runners/runShip.js +137 -207
- package/bin/runners/runStatus.js +16 -68
- package/bin/runners/utils.js +5 -5
- package/bin/vibecheck.js +25 -11
- package/mcp-server/index.js +150 -18
- package/mcp-server/package.json +2 -2
- package/mcp-server/premium-tools.js +13 -13
- package/mcp-server/tier-auth.js +292 -27
- package/mcp-server/vibecheck-tools.js +9 -9
- package/package.json +1 -1
- package/bin/runners/runClaimVerifier.js +0 -483
- package/bin/runners/runContextCompiler.js +0 -385
- package/bin/runners/runGate.js +0 -17
- package/bin/runners/runInitGha.js +0 -164
- package/bin/runners/runInteractive.js +0 -388
- package/bin/runners/runMdc.js +0 -204
- package/bin/runners/runMissionGenerator.js +0 -282
- package/bin/runners/runTruthpack.js +0 -636
package/bin/runners/runReport.js
CHANGED
|
@@ -1,63 +1,93 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* vibecheck report - World-Class
|
|
2
|
+
* vibecheck report - World-Class Enterprise Reports
|
|
3
3
|
*
|
|
4
4
|
* TIER ENFORCEMENT:
|
|
5
5
|
* - FREE: HTML, MD formats only
|
|
6
6
|
* - STARTER: + SARIF, CSV formats
|
|
7
|
-
* - PRO: + compliance packs, redaction templates
|
|
7
|
+
* - PRO: + compliance packs, PDF export, redaction templates
|
|
8
8
|
*
|
|
9
9
|
* Enterprise-grade report generation with:
|
|
10
10
|
* - Beautiful interactive HTML with modern design
|
|
11
|
-
* - Multiple export formats (HTML, MD, JSON, SARIF, CSV)
|
|
11
|
+
* - Multiple export formats (HTML, MD, JSON, SARIF, CSV, PDF)
|
|
12
12
|
* - Executive, Technical, Compliance report types
|
|
13
13
|
* - Historical trends and fix time estimates
|
|
14
14
|
* - Print-optimized layouts
|
|
15
|
+
* - White-label customization
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
const path = require("path");
|
|
18
19
|
const fs = require("fs");
|
|
19
20
|
|
|
20
21
|
// Entitlements enforcement
|
|
21
|
-
|
|
22
|
+
let entitlements;
|
|
23
|
+
try {
|
|
24
|
+
entitlements = require("./lib/entitlements-v2");
|
|
25
|
+
} catch {
|
|
26
|
+
// Fallback: allow all features if entitlements not available
|
|
27
|
+
entitlements = {
|
|
28
|
+
getLimits: () => ({ reportFormats: ["html", "md", "json", "sarif", "csv", "pdf"] }),
|
|
29
|
+
getTier: async () => "pro",
|
|
30
|
+
enforce: async () => ({ allowed: true }),
|
|
31
|
+
EXIT_FEATURE_NOT_ALLOWED: 1,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
22
34
|
|
|
23
|
-
// Report
|
|
24
|
-
let reportEngine, reportHtml;
|
|
35
|
+
// Report modules
|
|
36
|
+
let reportEngine, reportHtml, reportTemplates;
|
|
25
37
|
try {
|
|
26
38
|
reportEngine = require("./lib/report-engine");
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error("Warning: report-engine not found:", e.message);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
27
43
|
reportHtml = require("./lib/report-html");
|
|
28
44
|
} catch (e) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
console.error("Warning: report-html not found:", e.message);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
reportTemplates = require("./lib/report-templates");
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error("Warning: report-templates not found:", e.message);
|
|
32
51
|
}
|
|
33
52
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
};
|
|
53
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
54
|
+
// ENHANCED TERMINAL UI & OUTPUT MODULES
|
|
55
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
ansi,
|
|
59
|
+
colors,
|
|
60
|
+
icons,
|
|
61
|
+
Spinner,
|
|
62
|
+
renderSection,
|
|
63
|
+
formatDuration,
|
|
64
|
+
} = require("./lib/terminal-ui");
|
|
65
|
+
|
|
66
|
+
const {
|
|
67
|
+
BANNER,
|
|
68
|
+
formatHelp,
|
|
69
|
+
renderReportInfo,
|
|
70
|
+
renderSuccess,
|
|
71
|
+
renderError,
|
|
72
|
+
} = require("./lib/report-output");
|
|
46
73
|
|
|
47
74
|
function parseArgs(args) {
|
|
48
75
|
const opts = {
|
|
49
|
-
type: "
|
|
76
|
+
type: "technical",
|
|
50
77
|
format: "html",
|
|
51
78
|
path: ".",
|
|
52
79
|
output: null,
|
|
53
80
|
logo: null,
|
|
54
81
|
company: null,
|
|
55
82
|
theme: "dark",
|
|
83
|
+
framework: "SOC2",
|
|
56
84
|
includeVerify: false,
|
|
57
85
|
includeTrends: false,
|
|
58
86
|
redactPaths: false,
|
|
59
87
|
maxFindings: 50,
|
|
88
|
+
open: false,
|
|
60
89
|
help: false,
|
|
90
|
+
quiet: false,
|
|
61
91
|
};
|
|
62
92
|
|
|
63
93
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -68,6 +98,7 @@ function parseArgs(args) {
|
|
|
68
98
|
if (a === "--logo") opts.logo = args[++i];
|
|
69
99
|
if (a === "--company") opts.company = args[++i];
|
|
70
100
|
if (a === "--theme") opts.theme = args[++i];
|
|
101
|
+
if (a === "--framework") opts.framework = args[++i];
|
|
71
102
|
if (a === "--light") opts.theme = "light";
|
|
72
103
|
if (a === "--dark") opts.theme = "dark";
|
|
73
104
|
if (a === "--include-verify") opts.includeVerify = true;
|
|
@@ -76,6 +107,8 @@ function parseArgs(args) {
|
|
|
76
107
|
if (a === "--max-findings") opts.maxFindings = parseInt(args[++i]) || 50;
|
|
77
108
|
if (a.startsWith("--path=")) opts.path = a.split("=")[1];
|
|
78
109
|
if (a === "--path" || a === "-p") opts.path = args[++i];
|
|
110
|
+
if (a === "--open") opts.open = true;
|
|
111
|
+
if (a === "--quiet" || a === "-q") opts.quiet = true;
|
|
79
112
|
if (a === "--help" || a === "-h") opts.help = true;
|
|
80
113
|
}
|
|
81
114
|
|
|
@@ -83,60 +116,8 @@ function parseArgs(args) {
|
|
|
83
116
|
}
|
|
84
117
|
|
|
85
118
|
function printHelp() {
|
|
86
|
-
console.log(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
${c.dim}Generate beautiful, professional reports for stakeholders.${c.reset}
|
|
90
|
-
|
|
91
|
-
${c.bold}USAGE${c.reset}
|
|
92
|
-
vibecheck report Generate executive HTML report
|
|
93
|
-
vibecheck report --format=md Generate Markdown report
|
|
94
|
-
vibecheck report --format=sarif Generate SARIF for security tools
|
|
95
|
-
vibecheck report --type=compliance Generate compliance report
|
|
96
|
-
|
|
97
|
-
${c.bold}REPORT TYPES${c.reset}
|
|
98
|
-
${c.cyan}executive${c.reset} One-page overview for stakeholders (default)
|
|
99
|
-
${c.cyan}technical${c.reset} Detailed findings for developers/CTOs
|
|
100
|
-
${c.cyan}compliance${c.reset} SOC2/HIPAA-ready language for regulated industries
|
|
101
|
-
${c.cyan}trend${c.reset} Historical score analysis over time
|
|
102
|
-
|
|
103
|
-
${c.bold}OUTPUT FORMATS${c.reset}
|
|
104
|
-
${c.green}html${c.reset} Beautiful interactive report (default)
|
|
105
|
-
${c.green}md${c.reset} Markdown for documentation/GitHub
|
|
106
|
-
${c.green}json${c.reset} Machine-readable JSON
|
|
107
|
-
${c.green}sarif${c.reset} SARIF for security tool integration
|
|
108
|
-
${c.green}csv${c.reset} CSV for spreadsheet analysis
|
|
109
|
-
|
|
110
|
-
${c.bold}OPTIONS${c.reset}
|
|
111
|
-
--type, -t <type> Report type: executive, technical, compliance, trend
|
|
112
|
-
--format, -f <format> Output format: html, md, json, sarif, csv
|
|
113
|
-
--output, -o <path> Output file path
|
|
114
|
-
--theme <dark|light> HTML theme (default: dark)
|
|
115
|
-
--company <name> Company name for branding
|
|
116
|
-
--logo <path> Custom logo path/URL
|
|
117
|
-
--include-trends Include historical trend data
|
|
118
|
-
--redact-paths Hide file paths for client reports
|
|
119
|
-
--max-findings <n> Max findings to show (default: 50)
|
|
120
|
-
--path, -p <dir> Project path (default: current directory)
|
|
121
|
-
--help, -h Show this help
|
|
122
|
-
|
|
123
|
-
${c.bold}EXAMPLES${c.reset}
|
|
124
|
-
vibecheck report # Interactive HTML
|
|
125
|
-
vibecheck report --format=md --output=REPORT.md # Markdown file
|
|
126
|
-
vibecheck report --type=compliance --company=Acme # Compliance report
|
|
127
|
-
vibecheck report --format=sarif -o report.sarif # Security tooling
|
|
128
|
-
vibecheck report --theme=light --redact # Client-safe HTML
|
|
129
|
-
vibecheck report --trends # Include history
|
|
130
|
-
|
|
131
|
-
${c.bold}OUTPUT${c.reset}
|
|
132
|
-
HTML reports include:
|
|
133
|
-
• Animated score visualization
|
|
134
|
-
• Interactive severity charts
|
|
135
|
-
• Category breakdown bars
|
|
136
|
-
• Fix time estimates
|
|
137
|
-
• Dark/light mode toggle
|
|
138
|
-
• Print-optimized layout
|
|
139
|
-
`);
|
|
119
|
+
console.log(BANNER);
|
|
120
|
+
console.log(formatHelp());
|
|
140
121
|
}
|
|
141
122
|
|
|
142
123
|
async function runReport(args) {
|
|
@@ -151,26 +132,39 @@ async function runReport(args) {
|
|
|
151
132
|
const outputDir = path.join(projectPath, ".vibecheck");
|
|
152
133
|
const projectName = path.basename(projectPath);
|
|
153
134
|
|
|
154
|
-
// TIER ENFORCEMENT
|
|
135
|
+
// TIER ENFORCEMENT
|
|
155
136
|
const format = opts.format.toLowerCase();
|
|
156
|
-
const
|
|
137
|
+
const tier = await entitlements.getTier({ projectPath });
|
|
138
|
+
const limits = entitlements.getLimits(tier);
|
|
157
139
|
const allowedFormats = limits.reportFormats || ["html", "md"];
|
|
158
|
-
|
|
159
|
-
// Check
|
|
140
|
+
|
|
141
|
+
// Check format access
|
|
160
142
|
if (!allowedFormats.includes(format) && format !== "json") {
|
|
161
|
-
// SARIF/CSV require STARTER+
|
|
162
143
|
if (format === "sarif" || format === "csv") {
|
|
163
144
|
const access = await entitlements.enforce("report.sarif_csv", {
|
|
164
145
|
projectPath,
|
|
165
146
|
silent: false,
|
|
166
147
|
});
|
|
167
148
|
if (!access.allowed) {
|
|
168
|
-
console.log(`\n${
|
|
149
|
+
console.log(`\n ${colors.warning}${icons.warning}${ansi.reset} ${ansi.dim}HTML and MD formats are available on FREE tier${ansi.reset}`);
|
|
150
|
+
console.log(` ${ansi.dim}Upgrade to STARTER for SARIF/CSV export${ansi.reset}\n`);
|
|
151
|
+
return entitlements.EXIT_FEATURE_NOT_ALLOWED;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (format === "pdf") {
|
|
156
|
+
const access = await entitlements.enforce("report.pdf_export", {
|
|
157
|
+
projectPath,
|
|
158
|
+
silent: false,
|
|
159
|
+
});
|
|
160
|
+
if (!access.allowed) {
|
|
161
|
+
console.log(`\n ${colors.warning}${icons.warning}${ansi.reset} ${ansi.dim}PDF export requires PRO tier${ansi.reset}`);
|
|
162
|
+
console.log(` ${ansi.dim}HTML reports can be printed to PDF from browser${ansi.reset}\n`);
|
|
169
163
|
return entitlements.EXIT_FEATURE_NOT_ALLOWED;
|
|
170
164
|
}
|
|
171
165
|
}
|
|
172
166
|
}
|
|
173
|
-
|
|
167
|
+
|
|
174
168
|
// Compliance reports require PRO
|
|
175
169
|
if (opts.type === "compliance") {
|
|
176
170
|
const access = await entitlements.enforce("report.compliance_packs", {
|
|
@@ -178,23 +172,33 @@ async function runReport(args) {
|
|
|
178
172
|
silent: false,
|
|
179
173
|
});
|
|
180
174
|
if (!access.allowed) {
|
|
181
|
-
console.log(`\n${
|
|
175
|
+
console.log(`\n ${colors.warning}${icons.warning}${ansi.reset} ${ansi.dim}Executive and technical reports available on lower tiers${ansi.reset}`);
|
|
182
176
|
return entitlements.EXIT_FEATURE_NOT_ALLOWED;
|
|
183
177
|
}
|
|
184
178
|
}
|
|
185
179
|
|
|
186
|
-
|
|
180
|
+
// Display banner and report info
|
|
181
|
+
if (!opts.quiet) {
|
|
182
|
+
console.log(BANNER);
|
|
183
|
+
console.log(renderReportInfo(opts.type, opts.format, null));
|
|
184
|
+
console.log('');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Progress spinner
|
|
188
|
+
const spinner = new Spinner({ color: colors.accent });
|
|
189
|
+
spinner.start(`Generating ${opts.type} report`);
|
|
187
190
|
|
|
188
|
-
// Load ship results
|
|
191
|
+
// Load ship results
|
|
189
192
|
let shipResults = loadShipResults(projectPath, outputDir);
|
|
190
193
|
|
|
191
194
|
if (!shipResults) {
|
|
192
|
-
|
|
193
|
-
console.log(
|
|
195
|
+
spinner.warn("No scan results found - using demo data");
|
|
196
|
+
console.log(` ${ansi.dim}Tip: Run 'vibecheck ship' first for real results.${ansi.reset}\n`);
|
|
194
197
|
shipResults = getDemoData();
|
|
195
198
|
}
|
|
196
199
|
|
|
197
|
-
// Build comprehensive report data
|
|
200
|
+
// Build comprehensive report data
|
|
201
|
+
spinner.update("Processing findings");
|
|
198
202
|
let reportData;
|
|
199
203
|
if (reportEngine) {
|
|
200
204
|
reportData = reportEngine.buildReportData(shipResults, {
|
|
@@ -203,56 +207,73 @@ async function runReport(args) {
|
|
|
203
207
|
includeTrends: opts.includeTrends,
|
|
204
208
|
});
|
|
205
209
|
} else {
|
|
206
|
-
// Fallback to basic report data
|
|
207
210
|
reportData = buildBasicReportData(shipResults, projectName);
|
|
208
211
|
}
|
|
209
212
|
|
|
210
|
-
// Generate report content
|
|
213
|
+
// Generate report content
|
|
214
|
+
spinner.update(`Rendering ${format.toUpperCase()} output`);
|
|
211
215
|
let reportContent = "";
|
|
212
|
-
let fileExtension =
|
|
216
|
+
let fileExtension = format;
|
|
213
217
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
218
|
+
try {
|
|
219
|
+
switch (format) {
|
|
220
|
+
case "html":
|
|
221
|
+
reportContent = generateHTMLReport(reportData, opts);
|
|
222
|
+
break;
|
|
223
|
+
case "md":
|
|
224
|
+
case "markdown":
|
|
225
|
+
reportContent = generateMarkdownReport(reportData, opts);
|
|
226
|
+
fileExtension = "md";
|
|
227
|
+
break;
|
|
228
|
+
case "json":
|
|
229
|
+
reportContent = generateJSONReport(reportData, opts);
|
|
230
|
+
break;
|
|
231
|
+
case "sarif":
|
|
232
|
+
reportContent = generateSARIFReport(reportData, opts);
|
|
233
|
+
break;
|
|
234
|
+
case "csv":
|
|
235
|
+
reportContent = generateCSVReport(reportData, opts);
|
|
236
|
+
break;
|
|
237
|
+
case "pdf":
|
|
238
|
+
// PDF requires HTML first, then conversion
|
|
239
|
+
reportContent = await generatePDFReport(reportData, opts, outputDir);
|
|
240
|
+
break;
|
|
241
|
+
default:
|
|
242
|
+
spinner.fail(`Unknown format: ${format}`);
|
|
243
|
+
console.log(` ${ansi.dim}Supported: html, md, json, sarif, csv, pdf${ansi.reset}`);
|
|
244
|
+
return 1;
|
|
245
|
+
}
|
|
246
|
+
} catch (err) {
|
|
247
|
+
spinner.fail(`Failed to generate report: ${err.message}`);
|
|
248
|
+
return 1;
|
|
236
249
|
}
|
|
237
250
|
|
|
238
251
|
// Determine output path
|
|
239
252
|
const outputFileName = opts.output || path.join(outputDir, `report.${fileExtension}`);
|
|
253
|
+
const outputDirPath = path.dirname(outputFileName);
|
|
240
254
|
|
|
241
255
|
// Ensure output directory exists
|
|
242
|
-
const outputDirPath = path.dirname(outputFileName);
|
|
243
256
|
if (!fs.existsSync(outputDirPath)) {
|
|
244
257
|
fs.mkdirSync(outputDirPath, { recursive: true });
|
|
245
258
|
}
|
|
246
259
|
|
|
247
260
|
// Write report
|
|
261
|
+
spinner.update("Writing report");
|
|
248
262
|
fs.writeFileSync(outputFileName, reportContent);
|
|
249
263
|
|
|
250
264
|
// Save report history for trend analysis
|
|
251
265
|
saveReportHistory(outputDir, reportData);
|
|
252
266
|
|
|
253
|
-
|
|
254
|
-
console.log(
|
|
255
|
-
|
|
267
|
+
spinner.succeed("Report generated");
|
|
268
|
+
console.log("");
|
|
269
|
+
|
|
270
|
+
// Print success message using design system
|
|
271
|
+
console.log(renderSuccess(outputFileName, opts.format));
|
|
272
|
+
|
|
273
|
+
// Open in browser if requested
|
|
274
|
+
if (opts.open && format === "html") {
|
|
275
|
+
openInBrowser(outputFileName);
|
|
276
|
+
}
|
|
256
277
|
|
|
257
278
|
return 0;
|
|
258
279
|
}
|
|
@@ -262,7 +283,6 @@ async function runReport(args) {
|
|
|
262
283
|
// ============================================================================
|
|
263
284
|
|
|
264
285
|
function loadShipResults(projectPath, outputDir) {
|
|
265
|
-
// Try multiple sources for ship results
|
|
266
286
|
const sources = [
|
|
267
287
|
path.join(outputDir, "ship_report.json"),
|
|
268
288
|
path.join(outputDir, "report.json"),
|
|
@@ -286,7 +306,7 @@ function loadShipResults(projectPath, outputDir) {
|
|
|
286
306
|
path.join(outputDir, "reality", "last_reality.json"),
|
|
287
307
|
path.join(outputDir, "reality_report.json"),
|
|
288
308
|
];
|
|
289
|
-
|
|
309
|
+
|
|
290
310
|
for (const src of realitySources) {
|
|
291
311
|
if (fs.existsSync(src)) {
|
|
292
312
|
try {
|
|
@@ -311,29 +331,21 @@ function getDemoData() {
|
|
|
311
331
|
score: 72,
|
|
312
332
|
verdict: "WARN",
|
|
313
333
|
findings: [
|
|
314
|
-
{ id: "SEC001", severity: "BLOCK", type: "secret", message: "API key exposed in source code", file: "src/config.ts", line: 42, fix: "Move to environment variable" },
|
|
315
|
-
{ id: "AUTH001", severity: "BLOCK", type: "auth", message: "Authentication bypass in admin route", file: "src/routes/admin.ts", line: 15, fix: "Add auth middleware" },
|
|
316
|
-
{ id: "MOCK001", severity: "WARN", type: "mock", message: "Mock data used in production path", file: "src/api/users.ts", line: 88, fix: "Remove mock data fallback" },
|
|
317
|
-
{ id: "ERR001", severity: "WARN", type: "error", message: "Empty catch block in payment flow", file: "src/billing/checkout.ts", line: 156, fix: "Add error handling" },
|
|
318
|
-
{ id: "CFG001", severity: "INFO", type: "config", message: "Hardcoded timeout value", file: "src/utils/http.ts", line: 23, fix: "Move to config" },
|
|
319
|
-
{ id: "DBG001", severity: "INFO", type: "quality", message: "Console.log statement", file: "src/debug.ts", line: 5, fix: "Remove or use logger" },
|
|
334
|
+
{ id: "SEC001", severity: "BLOCK", type: "secret", message: "API key exposed in source code", title: "Exposed API Key", file: "src/config.ts", line: 42, fix: "Move to environment variable" },
|
|
335
|
+
{ id: "AUTH001", severity: "BLOCK", type: "auth", message: "Authentication bypass in admin route", title: "Auth Bypass Vulnerability", file: "src/routes/admin.ts", line: 15, fix: "Add auth middleware" },
|
|
336
|
+
{ id: "MOCK001", severity: "WARN", type: "mock", message: "Mock data used in production path", title: "Mock Data in Production", file: "src/api/users.ts", line: 88, fix: "Remove mock data fallback" },
|
|
337
|
+
{ id: "ERR001", severity: "WARN", type: "error", message: "Empty catch block in payment flow", title: "Swallowed Exception", file: "src/billing/checkout.ts", line: 156, fix: "Add error handling" },
|
|
338
|
+
{ id: "CFG001", severity: "INFO", type: "config", message: "Hardcoded timeout value", title: "Hardcoded Configuration", file: "src/utils/http.ts", line: 23, fix: "Move to config" },
|
|
339
|
+
{ id: "DBG001", severity: "INFO", type: "quality", message: "Console.log statement", title: "Debug Statement", file: "src/debug.ts", line: 5, fix: "Remove or use logger" },
|
|
320
340
|
],
|
|
341
|
+
categoryScores: { security: 65, auth: 50, billing: 90, quality: 75 },
|
|
321
342
|
truthpack: {
|
|
322
343
|
routes: { server: Array(12).fill({}) },
|
|
323
344
|
env: { vars: Array(8).fill("") },
|
|
324
|
-
auth: { nextMiddleware: [{}] },
|
|
325
|
-
billing: { webhooks: [{}] },
|
|
326
345
|
},
|
|
327
346
|
reality: {
|
|
328
|
-
coverage: {
|
|
329
|
-
clientCallsMapped: 87,
|
|
330
|
-
runtimeRequests: 72,
|
|
331
|
-
uiActionsVerified: 95,
|
|
332
|
-
authRoutes: 100,
|
|
333
|
-
},
|
|
334
|
-
requestsOverTime: [3, 5, 2, 8, 12, 6, 4, 9, 15, 7, 3, 10],
|
|
347
|
+
coverage: { clientCallsMapped: 87, uiActionsVerified: 95, authRoutes: 100 },
|
|
335
348
|
latencyP95: 412,
|
|
336
|
-
latencySparkline: [120, 150, 180, 200, 220, 280, 320, 380, 400, 412],
|
|
337
349
|
brokenFlows: [
|
|
338
350
|
{
|
|
339
351
|
title: "User cannot complete checkout flow",
|
|
@@ -346,21 +358,6 @@ function getDemoData() {
|
|
|
346
358
|
{ type: "error", label: "500 Internal Server Error" },
|
|
347
359
|
],
|
|
348
360
|
},
|
|
349
|
-
{
|
|
350
|
-
title: "Password reset email never sent",
|
|
351
|
-
severity: "BLOCK",
|
|
352
|
-
steps: [
|
|
353
|
-
{ type: "ui", label: "Click 'Forgot Password'" },
|
|
354
|
-
{ type: "form", label: "Enter email" },
|
|
355
|
-
{ type: "api", label: "POST /api/auth/reset" },
|
|
356
|
-
{ type: "error", label: "Missing SMTP config" },
|
|
357
|
-
],
|
|
358
|
-
},
|
|
359
|
-
],
|
|
360
|
-
unmappedRequests: [
|
|
361
|
-
{ method: "GET", path: "/api/analytics", count: 3 },
|
|
362
|
-
{ method: "POST", path: "/api/tracking", count: 5 },
|
|
363
|
-
{ method: "GET", path: "/api/health", count: 12 },
|
|
364
361
|
],
|
|
365
362
|
},
|
|
366
363
|
};
|
|
@@ -369,7 +366,7 @@ function getDemoData() {
|
|
|
369
366
|
function buildBasicReportData(shipResults, projectName) {
|
|
370
367
|
const findings = shipResults?.findings || [];
|
|
371
368
|
const reality = shipResults?.reality || null;
|
|
372
|
-
|
|
369
|
+
|
|
373
370
|
return {
|
|
374
371
|
meta: {
|
|
375
372
|
projectName,
|
|
@@ -387,11 +384,11 @@ function buildBasicReportData(shipResults, projectName) {
|
|
|
387
384
|
medium: findings.filter(f => f.severity === "WARN" || f.severity === "medium").length,
|
|
388
385
|
low: findings.filter(f => f.severity === "low" || f.severity === "INFO").length,
|
|
389
386
|
},
|
|
390
|
-
categoryScores: shipResults?.categoryScores || {
|
|
387
|
+
categoryScores: shipResults?.categoryScores || {},
|
|
391
388
|
},
|
|
392
|
-
findings: findings.map((f, i) => ({ ...f, id: f.id || `F${String(i+1).padStart(3,"0")}` })),
|
|
393
|
-
fixEstimates: { humanReadable:
|
|
394
|
-
truthpack:
|
|
389
|
+
findings: findings.map((f, i) => ({ ...f, id: f.id || `F${String(i + 1).padStart(3, "0")}` })),
|
|
390
|
+
fixEstimates: { humanReadable: calculateFixTime(findings), totalMinutes: 120 },
|
|
391
|
+
truthpack: shipResults?.truthpack || null,
|
|
395
392
|
reality: reality,
|
|
396
393
|
};
|
|
397
394
|
}
|
|
@@ -401,43 +398,51 @@ function buildBasicReportData(shipResults, projectName) {
|
|
|
401
398
|
// ============================================================================
|
|
402
399
|
|
|
403
400
|
function generateHTMLReport(reportData, opts) {
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
return
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
401
|
+
// Route to appropriate template based on type
|
|
402
|
+
switch (opts.type) {
|
|
403
|
+
case "executive":
|
|
404
|
+
if (reportTemplates?.generateEnhancedExecutiveReport) {
|
|
405
|
+
return reportTemplates.generateEnhancedExecutiveReport(
|
|
406
|
+
convertToLegacyFormat(reportData),
|
|
407
|
+
opts
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
case "compliance":
|
|
412
|
+
if (reportTemplates?.generateEnhancedComplianceReport) {
|
|
413
|
+
return reportTemplates.generateEnhancedComplianceReport(
|
|
414
|
+
convertToLegacyFormat(reportData),
|
|
415
|
+
opts
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
default:
|
|
420
|
+
// Technical report - use world-class HTML generator
|
|
421
|
+
if (reportHtml?.generateWorldClassHTML) {
|
|
422
|
+
return reportHtml.generateWorldClassHTML(reportData, opts);
|
|
423
|
+
}
|
|
422
424
|
}
|
|
425
|
+
|
|
426
|
+
// Fallback to basic HTML
|
|
427
|
+
return generateBasicHTML(reportData, opts);
|
|
423
428
|
}
|
|
424
429
|
|
|
425
430
|
function generateMarkdownReport(reportData, opts) {
|
|
426
|
-
if (reportEngine
|
|
431
|
+
if (reportEngine?.exportToMarkdown) {
|
|
427
432
|
return reportEngine.exportToMarkdown(reportData, opts);
|
|
428
433
|
}
|
|
429
434
|
return generateBasicMarkdown(reportData, opts);
|
|
430
435
|
}
|
|
431
436
|
|
|
432
437
|
function generateJSONReport(reportData, opts) {
|
|
433
|
-
if (reportEngine
|
|
438
|
+
if (reportEngine?.exportToJSON) {
|
|
434
439
|
return reportEngine.exportToJSON(reportData);
|
|
435
440
|
}
|
|
436
441
|
return JSON.stringify(reportData, null, 2);
|
|
437
442
|
}
|
|
438
443
|
|
|
439
444
|
function generateSARIFReport(reportData, opts) {
|
|
440
|
-
if (reportEngine
|
|
445
|
+
if (reportEngine?.exportToSARIF) {
|
|
441
446
|
return JSON.stringify(reportEngine.exportToSARIF(reportData), null, 2);
|
|
442
447
|
}
|
|
443
448
|
// Basic SARIF fallback
|
|
@@ -456,29 +461,66 @@ function generateSARIFReport(reportData, opts) {
|
|
|
456
461
|
}
|
|
457
462
|
|
|
458
463
|
function generateCSVReport(reportData, opts) {
|
|
459
|
-
if (reportEngine
|
|
464
|
+
if (reportEngine?.exportToCSV) {
|
|
460
465
|
return reportEngine.exportToCSV(reportData);
|
|
461
466
|
}
|
|
462
467
|
// Basic CSV fallback
|
|
463
|
-
const headers = ["ID", "Severity", "Title", "File", "Line"];
|
|
468
|
+
const headers = ["ID", "Severity", "Title", "File", "Line", "Fix"];
|
|
464
469
|
const rows = reportData.findings.map(f => [
|
|
465
470
|
f.id || "",
|
|
466
471
|
f.severity || "",
|
|
467
472
|
`"${(f.title || f.message || "").replace(/"/g, '""')}"`,
|
|
468
473
|
f.file || "",
|
|
469
474
|
f.line || "",
|
|
475
|
+
`"${(f.fix || "").replace(/"/g, '""')}"`,
|
|
470
476
|
]);
|
|
471
477
|
return [headers.join(","), ...rows.map(r => r.join(","))].join("\n");
|
|
472
478
|
}
|
|
473
479
|
|
|
480
|
+
async function generatePDFReport(reportData, opts, outputDir) {
|
|
481
|
+
// Generate HTML first
|
|
482
|
+
const htmlContent = generateHTMLReport(reportData, { ...opts, format: "html" });
|
|
483
|
+
const tempHtmlPath = path.join(outputDir, "temp_report.html");
|
|
484
|
+
fs.writeFileSync(tempHtmlPath, htmlContent);
|
|
485
|
+
|
|
486
|
+
// Try to use puppeteer for PDF generation
|
|
487
|
+
try {
|
|
488
|
+
const puppeteer = require("puppeteer");
|
|
489
|
+
const browser = await puppeteer.launch({ headless: "new" });
|
|
490
|
+
const page = await browser.newPage();
|
|
491
|
+
await page.setContent(htmlContent, { waitUntil: "networkidle0" });
|
|
492
|
+
const pdf = await page.pdf({
|
|
493
|
+
format: "Letter",
|
|
494
|
+
printBackground: true,
|
|
495
|
+
margin: { top: "0.5in", right: "0.5in", bottom: "0.5in", left: "0.5in" },
|
|
496
|
+
});
|
|
497
|
+
await browser.close();
|
|
498
|
+
|
|
499
|
+
// Clean up temp file
|
|
500
|
+
try { fs.unlinkSync(tempHtmlPath); } catch {}
|
|
501
|
+
|
|
502
|
+
return pdf;
|
|
503
|
+
} catch (e) {
|
|
504
|
+
// Clean up temp file
|
|
505
|
+
try { fs.unlinkSync(tempHtmlPath); } catch {}
|
|
506
|
+
|
|
507
|
+
throw new Error(
|
|
508
|
+
`PDF generation requires puppeteer. Install with: npm install puppeteer\n` +
|
|
509
|
+
`Or use --format=html and print to PDF from your browser.`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
474
514
|
function convertToLegacyFormat(reportData) {
|
|
475
515
|
return {
|
|
476
516
|
projectName: reportData.meta.projectName,
|
|
477
517
|
generatedAt: reportData.meta.generatedAt,
|
|
518
|
+
reportId: reportData.meta.reportId,
|
|
478
519
|
score: reportData.summary.score,
|
|
479
520
|
verdict: reportData.summary.verdict,
|
|
480
521
|
findings: reportData.findings,
|
|
481
522
|
categoryScores: reportData.summary.categoryScores,
|
|
523
|
+
reality: reportData.reality,
|
|
482
524
|
};
|
|
483
525
|
}
|
|
484
526
|
|
|
@@ -488,18 +530,18 @@ function convertToLegacyFormat(reportData) {
|
|
|
488
530
|
|
|
489
531
|
function generateBasicHTML(reportData, opts) {
|
|
490
532
|
const { meta, summary } = reportData;
|
|
491
|
-
const verdictColor = summary.verdict === "SHIP" ? "#
|
|
492
|
-
|
|
533
|
+
const verdictColor = summary.verdict === "SHIP" ? "#10b981" : summary.verdict === "WARN" ? "#f59e0b" : "#ef4444";
|
|
534
|
+
|
|
493
535
|
return `<!DOCTYPE html>
|
|
494
536
|
<html lang="en">
|
|
495
537
|
<head>
|
|
496
538
|
<meta charset="UTF-8">
|
|
497
539
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
498
|
-
<title>
|
|
540
|
+
<title>VibeCheck Report - ${meta.projectName}</title>
|
|
499
541
|
<style>
|
|
500
542
|
body { font-family: -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 40px; background: #0f172a; color: #f8fafc; }
|
|
501
543
|
.header { border-bottom: 2px solid #334155; padding-bottom: 20px; margin-bottom: 30px; }
|
|
502
|
-
.score { font-size: 4rem; font-weight: 800; text-align: center; margin: 40px 0; }
|
|
544
|
+
.score { font-size: 4rem; font-weight: 800; text-align: center; margin: 40px 0; color: ${verdictColor}; }
|
|
503
545
|
.verdict { display: inline-block; padding: 12px 24px; border-radius: 50px; font-weight: 600; background: ${verdictColor}22; color: ${verdictColor}; }
|
|
504
546
|
.section { margin: 30px 0; }
|
|
505
547
|
h2 { color: #94a3b8; font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.1em; }
|
|
@@ -518,19 +560,19 @@ function generateBasicHTML(reportData, opts) {
|
|
|
518
560
|
<h2>Findings (${summary.totalFindings})</h2>
|
|
519
561
|
${reportData.findings.slice(0, 10).map(f => `
|
|
520
562
|
<div class="finding">
|
|
521
|
-
<strong>${f.severity
|
|
563
|
+
<strong>${(f.severity || 'WARN').toUpperCase()}</strong>: ${f.title || f.message}
|
|
522
564
|
${f.file ? `<br><code>${f.file}${f.line ? `:${f.line}` : ""}</code>` : ""}
|
|
523
565
|
</div>
|
|
524
566
|
`).join("")}
|
|
525
567
|
</div>
|
|
526
|
-
<div class="footer">Generated by
|
|
568
|
+
<div class="footer">Generated by VibeCheck · ${meta.reportId}</div>
|
|
527
569
|
</body>
|
|
528
570
|
</html>`;
|
|
529
571
|
}
|
|
530
572
|
|
|
531
573
|
function generateBasicMarkdown(reportData, opts) {
|
|
532
574
|
const { meta, summary, findings } = reportData;
|
|
533
|
-
return `#
|
|
575
|
+
return `# VibeCheck Report
|
|
534
576
|
|
|
535
577
|
**Project:** ${meta.projectName}
|
|
536
578
|
**Generated:** ${new Date(meta.generatedAt).toLocaleString()}
|
|
@@ -539,10 +581,10 @@ function generateBasicMarkdown(reportData, opts) {
|
|
|
539
581
|
|
|
540
582
|
## Findings (${summary.totalFindings})
|
|
541
583
|
|
|
542
|
-
${findings.slice(0, 20).map(f => `- **${f.severity
|
|
584
|
+
${findings.slice(0, 20).map(f => `- **${(f.severity || 'WARN').toUpperCase()}**: ${f.title || f.message}${f.file ? ` (\`${f.file}\`)` : ""}`).join("\n")}
|
|
543
585
|
|
|
544
586
|
---
|
|
545
|
-
*Generated by
|
|
587
|
+
*Generated by VibeCheck · ${meta.reportId}*
|
|
546
588
|
`;
|
|
547
589
|
}
|
|
548
590
|
|
|
@@ -550,16 +592,30 @@ ${findings.slice(0, 20).map(f => `- **${f.severity?.toUpperCase()}**: ${f.title
|
|
|
550
592
|
// UTILITIES
|
|
551
593
|
// ============================================================================
|
|
552
594
|
|
|
595
|
+
function calculateFixTime(findings) {
|
|
596
|
+
if (!findings || findings.length === 0) return "0h";
|
|
597
|
+
|
|
598
|
+
const minutes = findings.reduce((total, f) => {
|
|
599
|
+
const sev = (f.severity || "").toLowerCase();
|
|
600
|
+
const time = { block: 60, critical: 60, high: 45, warn: 20, medium: 20, info: 10, low: 10 };
|
|
601
|
+
return total + (time[sev] || 15);
|
|
602
|
+
}, 0);
|
|
603
|
+
|
|
604
|
+
if (minutes < 60) return `${minutes}m`;
|
|
605
|
+
const hours = Math.round(minutes / 60 * 10) / 10;
|
|
606
|
+
return hours > 8 ? `${Math.ceil(hours / 8)}d` : `${hours}h`;
|
|
607
|
+
}
|
|
608
|
+
|
|
553
609
|
function saveReportHistory(outputDir, reportData) {
|
|
554
610
|
try {
|
|
555
611
|
const historyDir = path.join(outputDir, "history");
|
|
556
612
|
if (!fs.existsSync(historyDir)) {
|
|
557
613
|
fs.mkdirSync(historyDir, { recursive: true });
|
|
558
614
|
}
|
|
559
|
-
|
|
615
|
+
|
|
560
616
|
const timestamp = new Date().toISOString().split("T")[0];
|
|
561
617
|
const historyFile = path.join(historyDir, `${timestamp}.json`);
|
|
562
|
-
|
|
618
|
+
|
|
563
619
|
fs.writeFileSync(historyFile, JSON.stringify({
|
|
564
620
|
meta: reportData.meta,
|
|
565
621
|
summary: reportData.summary,
|
|
@@ -569,16 +625,11 @@ function saveReportHistory(outputDir, reportData) {
|
|
|
569
625
|
}
|
|
570
626
|
}
|
|
571
627
|
|
|
572
|
-
function
|
|
573
|
-
const {
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
console.log(` ${c.dim}Format:${c.reset} ${c.bold}${opts.format}${c.reset}`);
|
|
578
|
-
console.log(` ${c.dim}Score:${c.reset} ${verdictColor}${c.bold}${summary.score}${c.reset}/100`);
|
|
579
|
-
console.log(` ${c.dim}Verdict:${c.reset} ${verdictColor}${c.bold}${summary.verdict}${c.reset}`);
|
|
580
|
-
console.log(` ${c.dim}Findings:${c.reset} ${summary.totalFindings}`);
|
|
581
|
-
console.log(`\n ${c.dim}Output:${c.reset} ${c.cyan}${outputFile}${c.reset}\n`);
|
|
628
|
+
function openInBrowser(filePath) {
|
|
629
|
+
const { exec } = require("child_process");
|
|
630
|
+
const cmd = process.platform === "darwin" ? "open" :
|
|
631
|
+
process.platform === "win32" ? "start" : "xdg-open";
|
|
632
|
+
exec(`${cmd} "${filePath}"`);
|
|
582
633
|
}
|
|
583
634
|
|
|
584
635
|
module.exports = { runReport };
|