@vibecheckai/cli 3.1.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/.generated +25 -25
- package/bin/dev/run-v2-torture.js +30 -30
- package/bin/registry.js +105 -105
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
- package/bin/runners/lib/analysis-core.js +271 -271
- package/bin/runners/lib/analyzers.js +579 -579
- package/bin/runners/lib/auth-truth.js +193 -193
- package/bin/runners/lib/backup.js +62 -62
- package/bin/runners/lib/billing.js +107 -107
- package/bin/runners/lib/claims.js +118 -118
- package/bin/runners/lib/cli-output.js +368 -368
- package/bin/runners/lib/cli-ui.js +540 -540
- package/bin/runners/lib/contracts/auth-contract.js +202 -202
- package/bin/runners/lib/contracts/env-contract.js +181 -181
- package/bin/runners/lib/contracts/external-contract.js +206 -206
- package/bin/runners/lib/contracts/guard.js +168 -168
- package/bin/runners/lib/contracts/index.js +89 -89
- package/bin/runners/lib/contracts/plan-validator.js +311 -311
- package/bin/runners/lib/contracts/route-contract.js +199 -199
- package/bin/runners/lib/contracts.js +804 -804
- package/bin/runners/lib/detect.js +89 -89
- package/bin/runners/lib/detectors-v2.js +703 -703
- package/bin/runners/lib/doctor/autofix.js +254 -254
- package/bin/runners/lib/doctor/index.js +37 -37
- package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
- package/bin/runners/lib/doctor/modules/index.js +46 -46
- package/bin/runners/lib/doctor/modules/network.js +250 -250
- package/bin/runners/lib/doctor/modules/project.js +312 -312
- package/bin/runners/lib/doctor/modules/runtime.js +224 -224
- package/bin/runners/lib/doctor/modules/security.js +348 -348
- package/bin/runners/lib/doctor/modules/system.js +213 -213
- package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
- package/bin/runners/lib/doctor/reporter.js +262 -262
- package/bin/runners/lib/doctor/service.js +262 -262
- package/bin/runners/lib/doctor/types.js +113 -113
- package/bin/runners/lib/doctor/ui.js +263 -263
- package/bin/runners/lib/doctor-v2.js +608 -608
- package/bin/runners/lib/drift.js +425 -425
- package/bin/runners/lib/enforcement.js +72 -72
- package/bin/runners/lib/enterprise-detect.js +603 -603
- package/bin/runners/lib/enterprise-init.js +942 -942
- package/bin/runners/lib/entitlements-v2.js +490 -489
- package/bin/runners/lib/env-resolver.js +417 -417
- package/bin/runners/lib/env-template.js +66 -66
- package/bin/runners/lib/env.js +189 -189
- package/bin/runners/lib/extractors/client-calls.js +990 -990
- package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
- package/bin/runners/lib/extractors/fastify-routes.js +426 -426
- package/bin/runners/lib/extractors/index.js +363 -363
- package/bin/runners/lib/extractors/next-routes.js +524 -524
- package/bin/runners/lib/extractors/proof-graph.js +431 -431
- package/bin/runners/lib/extractors/route-matcher.js +451 -451
- package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
- package/bin/runners/lib/extractors/ui-bindings.js +547 -547
- package/bin/runners/lib/findings-schema.js +281 -281
- package/bin/runners/lib/firewall-prompt.js +50 -50
- package/bin/runners/lib/graph/graph-builder.js +265 -265
- package/bin/runners/lib/graph/html-renderer.js +413 -413
- package/bin/runners/lib/graph/index.js +32 -32
- package/bin/runners/lib/graph/runtime-collector.js +215 -215
- package/bin/runners/lib/graph/static-extractor.js +518 -518
- package/bin/runners/lib/html-report.js +650 -650
- package/bin/runners/lib/init-wizard.js +308 -308
- package/bin/runners/lib/llm.js +75 -75
- package/bin/runners/lib/meter.js +61 -61
- package/bin/runners/lib/missions/evidence.js +126 -126
- package/bin/runners/lib/missions/plan.js +69 -69
- package/bin/runners/lib/missions/templates.js +192 -192
- package/bin/runners/lib/patch.js +40 -40
- package/bin/runners/lib/permissions/auth-model.js +213 -213
- package/bin/runners/lib/permissions/idor-prover.js +205 -205
- package/bin/runners/lib/permissions/index.js +45 -45
- package/bin/runners/lib/permissions/matrix-builder.js +198 -198
- package/bin/runners/lib/pkgjson.js +28 -28
- package/bin/runners/lib/policy.js +295 -295
- package/bin/runners/lib/preflight.js +142 -142
- package/bin/runners/lib/reality/correlation-detectors.js +359 -359
- package/bin/runners/lib/reality/index.js +318 -318
- package/bin/runners/lib/reality/request-hashing.js +416 -416
- package/bin/runners/lib/reality/request-mapper.js +453 -453
- package/bin/runners/lib/reality/safety-rails.js +463 -463
- package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
- package/bin/runners/lib/reality/toast-detector.js +393 -393
- package/bin/runners/lib/reality-findings.js +84 -84
- package/bin/runners/lib/receipts.js +179 -179
- package/bin/runners/lib/redact.js +29 -29
- package/bin/runners/lib/replay/capsule-manager.js +154 -154
- package/bin/runners/lib/replay/index.js +263 -263
- package/bin/runners/lib/replay/player.js +348 -348
- package/bin/runners/lib/replay/recorder.js +331 -331
- package/bin/runners/lib/report-engine.js +447 -447
- package/bin/runners/lib/report-html.js +1499 -1499
- package/bin/runners/lib/report-templates.js +969 -969
- package/bin/runners/lib/report.js +135 -135
- package/bin/runners/lib/route-detection.js +1140 -1140
- package/bin/runners/lib/route-truth.js +477 -477
- package/bin/runners/lib/sandbox/index.js +59 -59
- package/bin/runners/lib/sandbox/proof-chain.js +399 -399
- package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
- package/bin/runners/lib/sandbox/worktree.js +174 -174
- package/bin/runners/lib/schema-validator.js +350 -350
- package/bin/runners/lib/schemas/contracts.schema.json +160 -160
- package/bin/runners/lib/schemas/finding.schema.json +100 -100
- package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
- package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
- package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
- package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
- package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
- package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
- package/bin/runners/lib/schemas/validator.js +438 -438
- package/bin/runners/lib/score-history.js +282 -282
- package/bin/runners/lib/share-pack.js +239 -239
- package/bin/runners/lib/snippets.js +67 -67
- package/bin/runners/lib/truth.js +667 -667
- package/bin/runners/lib/upsell.js +510 -510
- package/bin/runners/lib/usage.js +153 -153
- package/bin/runners/lib/validate-patch.js +156 -156
- package/bin/runners/lib/verdict-engine.js +628 -628
- package/bin/runners/reality/engine.js +917 -917
- package/bin/runners/reality/flows.js +122 -122
- package/bin/runners/reality/report.js +378 -378
- package/bin/runners/reality/session.js +193 -193
- package/bin/runners/runAuth.js +51 -0
- package/bin/runners/runClaimVerifier.js +483 -483
- package/bin/runners/runContext.js +56 -56
- package/bin/runners/runContextCompiler.js +385 -385
- package/bin/runners/runCtx.js +674 -674
- package/bin/runners/runCtxDiff.js +301 -301
- package/bin/runners/runCtxGuard.js +176 -176
- package/bin/runners/runCtxSync.js +116 -116
- package/bin/runners/runGate.js +17 -17
- package/bin/runners/runGraph.js +454 -454
- package/bin/runners/runGuard.js +168 -168
- package/bin/runners/runInitGha.js +164 -164
- package/bin/runners/runInstall.js +277 -277
- package/bin/runners/runInteractive.js +388 -388
- package/bin/runners/runLabs.js +340 -340
- package/bin/runners/runMissionGenerator.js +282 -282
- package/bin/runners/runPR.js +255 -255
- package/bin/runners/runPermissions.js +304 -304
- package/bin/runners/runPreflight.js +580 -553
- package/bin/runners/runProve.js +1252 -1252
- package/bin/runners/runReality.js +1328 -1328
- package/bin/runners/runReplay.js +499 -499
- package/bin/runners/runReport.js +584 -584
- package/bin/runners/runShare.js +212 -212
- package/bin/runners/runStatus.js +138 -138
- package/bin/runners/runTruthpack.js +636 -636
- package/bin/runners/runVerify.js +272 -272
- package/bin/runners/runWatch.js +407 -407
- package/bin/vibecheck.js +2 -1
- package/mcp-server/consolidated-tools.js +804 -804
- package/mcp-server/package.json +1 -1
- package/mcp-server/tools/index.js +72 -72
- package/mcp-server/truth-context.js +581 -581
- package/mcp-server/truth-firewall-tools.js +1500 -1500
- package/package.json +1 -1
- package/bin/runners/runProof.zip +0 -0
|
@@ -1,447 +1,447 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Report Engine - World-Class Report Generation
|
|
3
|
-
*
|
|
4
|
-
* Enterprise-grade reporting with:
|
|
5
|
-
* - Multiple output formats (HTML, PDF, MD, JSON, SARIF, CSV)
|
|
6
|
-
* - Beautiful interactive HTML with Chart.js
|
|
7
|
-
* - Executive, Technical, Compliance, and Trend reports
|
|
8
|
-
* - Historical analysis and fix time estimates
|
|
9
|
-
* - Print-optimized layouts
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const fs = require("fs");
|
|
13
|
-
const path = require("path");
|
|
14
|
-
|
|
15
|
-
// ============================================================================
|
|
16
|
-
// REPORT DATA MODEL
|
|
17
|
-
// ============================================================================
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Build comprehensive report data from ship results
|
|
21
|
-
*/
|
|
22
|
-
function buildReportData(shipResults, options = {}) {
|
|
23
|
-
const findings = shipResults?.findings || [];
|
|
24
|
-
const truthpack = shipResults?.truthpack || {};
|
|
25
|
-
|
|
26
|
-
// Severity counts
|
|
27
|
-
const severityCounts = {
|
|
28
|
-
critical: findings.filter(f => f.severity === "BLOCK" || f.severity === "critical").length,
|
|
29
|
-
high: findings.filter(f => f.severity === "high").length,
|
|
30
|
-
medium: findings.filter(f => f.severity === "WARN" || f.severity === "medium").length,
|
|
31
|
-
low: findings.filter(f => f.severity === "low" || f.severity === "INFO").length,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// Category scores (invert finding counts to scores)
|
|
35
|
-
const categoryFindings = groupFindingsByCategory(findings);
|
|
36
|
-
const categoryScores = calculateCategoryScores(categoryFindings);
|
|
37
|
-
|
|
38
|
-
// Overall score
|
|
39
|
-
const score = calculateOverallScore(severityCounts, categoryScores);
|
|
40
|
-
|
|
41
|
-
// Verdict
|
|
42
|
-
const verdict = severityCounts.critical > 0 ? "BLOCK" :
|
|
43
|
-
severityCounts.high > 0 || severityCounts.medium > 5 ? "WARN" : "SHIP";
|
|
44
|
-
|
|
45
|
-
// Fix time estimates
|
|
46
|
-
const fixEstimates = estimateFixTimes(findings);
|
|
47
|
-
|
|
48
|
-
// Coverage stats
|
|
49
|
-
const coverage = extractCoverageStats(truthpack);
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
meta: {
|
|
53
|
-
projectName: options.projectName || truthpack?.project?.name || path.basename(process.cwd()),
|
|
54
|
-
generatedAt: new Date().toISOString(),
|
|
55
|
-
version: "2.0.0",
|
|
56
|
-
reportId: generateReportId(),
|
|
57
|
-
},
|
|
58
|
-
summary: {
|
|
59
|
-
score,
|
|
60
|
-
verdict,
|
|
61
|
-
totalFindings: findings.length,
|
|
62
|
-
severityCounts,
|
|
63
|
-
categoryScores,
|
|
64
|
-
},
|
|
65
|
-
findings: enrichFindings(findings),
|
|
66
|
-
coverage,
|
|
67
|
-
fixEstimates,
|
|
68
|
-
truthpack: {
|
|
69
|
-
routes: truthpack?.routes?.server?.length || 0,
|
|
70
|
-
envVars: truthpack?.env?.vars?.length || 0,
|
|
71
|
-
hasAuth: !!(truthpack?.auth?.nextMiddleware?.length || truthpack?.auth?.fastify?.hooks?.length),
|
|
72
|
-
hasBilling: !!truthpack?.billing?.webhooks?.length,
|
|
73
|
-
},
|
|
74
|
-
trends: options.includeTrends ? loadHistoricalTrends(options.repoRoot) : null,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function groupFindingsByCategory(findings) {
|
|
79
|
-
const groups = {
|
|
80
|
-
security: [],
|
|
81
|
-
auth: [],
|
|
82
|
-
billing: [],
|
|
83
|
-
routes: [],
|
|
84
|
-
env: [],
|
|
85
|
-
quality: [],
|
|
86
|
-
other: [],
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
for (const f of findings) {
|
|
90
|
-
const cat = categorizeFindings(f);
|
|
91
|
-
if (groups[cat]) {
|
|
92
|
-
groups[cat].push(f);
|
|
93
|
-
} else {
|
|
94
|
-
groups.other.push(f);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return groups;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function categorizeFindings(finding) {
|
|
102
|
-
const type = (finding.type || finding.id || "").toLowerCase();
|
|
103
|
-
const msg = (finding.message || finding.title || "").toLowerCase();
|
|
104
|
-
|
|
105
|
-
if (type.includes("auth") || msg.includes("auth") || msg.includes("session")) return "auth";
|
|
106
|
-
if (type.includes("billing") || type.includes("stripe") || msg.includes("payment")) return "billing";
|
|
107
|
-
if (type.includes("route") || type.includes("endpoint") || msg.includes("route")) return "routes";
|
|
108
|
-
if (type.includes("env") || msg.includes("environment")) return "env";
|
|
109
|
-
if (type.includes("secret") || type.includes("security") || msg.includes("secret")) return "security";
|
|
110
|
-
if (type.includes("mock") || type.includes("fake") || msg.includes("console")) return "quality";
|
|
111
|
-
return "other";
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function calculateCategoryScores(categoryFindings) {
|
|
115
|
-
const scores = {};
|
|
116
|
-
const weights = {
|
|
117
|
-
security: { max: 100, perFinding: 20 },
|
|
118
|
-
auth: { max: 100, perFinding: 15 },
|
|
119
|
-
billing: { max: 100, perFinding: 15 },
|
|
120
|
-
routes: { max: 100, perFinding: 10 },
|
|
121
|
-
env: { max: 100, perFinding: 5 },
|
|
122
|
-
quality: { max: 100, perFinding: 5 },
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
for (const [cat, findings] of Object.entries(categoryFindings)) {
|
|
126
|
-
if (cat === "other") continue;
|
|
127
|
-
const w = weights[cat] || { max: 100, perFinding: 10 };
|
|
128
|
-
scores[cat] = Math.max(0, w.max - (findings.length * w.perFinding));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return scores;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function calculateOverallScore(severityCounts, categoryScores) {
|
|
135
|
-
// Weighted average of category scores with severity penalties
|
|
136
|
-
const categoryAvg = Object.values(categoryScores).reduce((a, b) => a + b, 0) /
|
|
137
|
-
Math.max(1, Object.keys(categoryScores).length);
|
|
138
|
-
|
|
139
|
-
// Severity penalties
|
|
140
|
-
const penalties =
|
|
141
|
-
(severityCounts.critical * 25) +
|
|
142
|
-
(severityCounts.high * 10) +
|
|
143
|
-
(severityCounts.medium * 3) +
|
|
144
|
-
(severityCounts.low * 1);
|
|
145
|
-
|
|
146
|
-
return Math.max(0, Math.min(100, Math.round(categoryAvg - penalties)));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function enrichFindings(findings) {
|
|
150
|
-
return findings.map((f, idx) => ({
|
|
151
|
-
id: f.id || `F${String(idx + 1).padStart(3, "0")}`,
|
|
152
|
-
severity: normalizeSeverity(f.severity),
|
|
153
|
-
category: categorizeFindings(f),
|
|
154
|
-
title: f.title || f.message || "Finding",
|
|
155
|
-
description: f.description || f.message || "",
|
|
156
|
-
file: f.file || f.evidence?.file || null,
|
|
157
|
-
line: f.line || f.evidence?.line || null,
|
|
158
|
-
fix: f.fix || f.recommendation || null,
|
|
159
|
-
fixTime: estimateSingleFixTime(f),
|
|
160
|
-
evidence: f.evidence || null,
|
|
161
|
-
}));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function normalizeSeverity(sev) {
|
|
165
|
-
const s = String(sev || "medium").toLowerCase();
|
|
166
|
-
if (s === "block" || s === "critical") return "critical";
|
|
167
|
-
if (s === "high") return "high";
|
|
168
|
-
if (s === "warn" || s === "warning" || s === "medium") return "medium";
|
|
169
|
-
return "low";
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function estimateFixTimes(findings) {
|
|
173
|
-
const times = {
|
|
174
|
-
critical: { count: 0, totalMinutes: 0 },
|
|
175
|
-
high: { count: 0, totalMinutes: 0 },
|
|
176
|
-
medium: { count: 0, totalMinutes: 0 },
|
|
177
|
-
low: { count: 0, totalMinutes: 0 },
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
for (const f of findings) {
|
|
181
|
-
const sev = normalizeSeverity(f.severity);
|
|
182
|
-
const mins = estimateSingleFixTime(f);
|
|
183
|
-
if (times[sev]) {
|
|
184
|
-
times[sev].count++;
|
|
185
|
-
times[sev].totalMinutes += mins;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const totalMinutes = Object.values(times).reduce((a, t) => a + t.totalMinutes, 0);
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
bySeverity: times,
|
|
193
|
-
totalMinutes,
|
|
194
|
-
totalHours: Math.round(totalMinutes / 60 * 10) / 10,
|
|
195
|
-
humanReadable: formatDuration(totalMinutes),
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function estimateSingleFixTime(finding) {
|
|
200
|
-
const sev = normalizeSeverity(finding.severity);
|
|
201
|
-
const type = (finding.type || finding.id || "").toLowerCase();
|
|
202
|
-
|
|
203
|
-
// Base times by severity
|
|
204
|
-
const baseTimes = { critical: 60, high: 30, medium: 15, low: 5 };
|
|
205
|
-
let mins = baseTimes[sev] || 15;
|
|
206
|
-
|
|
207
|
-
// Adjustments by type
|
|
208
|
-
if (type.includes("auth") || type.includes("security")) mins *= 1.5;
|
|
209
|
-
if (type.includes("mock") || type.includes("console")) mins *= 0.5;
|
|
210
|
-
|
|
211
|
-
return Math.round(mins);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function formatDuration(minutes) {
|
|
215
|
-
if (minutes < 60) return `${minutes} min`;
|
|
216
|
-
const hours = Math.floor(minutes / 60);
|
|
217
|
-
const mins = minutes % 60;
|
|
218
|
-
if (mins === 0) return `${hours}h`;
|
|
219
|
-
return `${hours}h ${mins}m`;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function extractCoverageStats(truthpack) {
|
|
223
|
-
return {
|
|
224
|
-
routes: {
|
|
225
|
-
total: truthpack?.routes?.server?.length || 0,
|
|
226
|
-
protected: truthpack?.routes?.server?.filter(r => r.protected)?.length || 0,
|
|
227
|
-
tested: truthpack?.routes?.coverage?.hit || 0,
|
|
228
|
-
},
|
|
229
|
-
env: {
|
|
230
|
-
total: truthpack?.env?.vars?.length || 0,
|
|
231
|
-
declared: truthpack?.env?.declared?.length || 0,
|
|
232
|
-
},
|
|
233
|
-
auth: {
|
|
234
|
-
middlewareCount: truthpack?.auth?.nextMiddleware?.length || 0,
|
|
235
|
-
patterns: truthpack?.auth?.nextMatcherPatterns?.length || 0,
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function generateReportId() {
|
|
241
|
-
const date = new Date().toISOString().split("T")[0].replace(/-/g, "");
|
|
242
|
-
const rand = Math.random().toString(36).substring(2, 8).toUpperCase();
|
|
243
|
-
return `VC-${date}-${rand}`;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function loadHistoricalTrends(repoRoot) {
|
|
247
|
-
// Try to load previous reports for trend analysis
|
|
248
|
-
const historyPath = path.join(repoRoot || process.cwd(), ".vibecheck", "history");
|
|
249
|
-
if (!fs.existsSync(historyPath)) return null;
|
|
250
|
-
|
|
251
|
-
try {
|
|
252
|
-
const files = fs.readdirSync(historyPath)
|
|
253
|
-
.filter(f => f.endsWith(".json"))
|
|
254
|
-
.sort()
|
|
255
|
-
.slice(-10);
|
|
256
|
-
|
|
257
|
-
return files.map(f => {
|
|
258
|
-
const data = JSON.parse(fs.readFileSync(path.join(historyPath, f), "utf8"));
|
|
259
|
-
return {
|
|
260
|
-
date: data.meta?.generatedAt || f.replace(".json", ""),
|
|
261
|
-
score: data.summary?.score || 0,
|
|
262
|
-
findings: data.summary?.totalFindings || 0,
|
|
263
|
-
};
|
|
264
|
-
});
|
|
265
|
-
} catch {
|
|
266
|
-
return null;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// ============================================================================
|
|
271
|
-
// EXPORT FORMATS
|
|
272
|
-
// ============================================================================
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Export to SARIF format (Static Analysis Results Interchange Format)
|
|
276
|
-
*/
|
|
277
|
-
function exportToSARIF(reportData) {
|
|
278
|
-
return {
|
|
279
|
-
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
280
|
-
version: "2.1.0",
|
|
281
|
-
runs: [{
|
|
282
|
-
tool: {
|
|
283
|
-
driver: {
|
|
284
|
-
name: "vibecheck",
|
|
285
|
-
version: reportData.meta.version,
|
|
286
|
-
informationUri: "https://vibecheck.dev",
|
|
287
|
-
rules: generateSARIFRules(reportData.findings),
|
|
288
|
-
},
|
|
289
|
-
},
|
|
290
|
-
results: reportData.findings.map(f => ({
|
|
291
|
-
ruleId: f.id,
|
|
292
|
-
level: sarifLevel(f.severity),
|
|
293
|
-
message: { text: f.title },
|
|
294
|
-
locations: f.file ? [{
|
|
295
|
-
physicalLocation: {
|
|
296
|
-
artifactLocation: { uri: f.file },
|
|
297
|
-
region: f.line ? { startLine: f.line } : undefined,
|
|
298
|
-
},
|
|
299
|
-
}] : [],
|
|
300
|
-
})),
|
|
301
|
-
}],
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function generateSARIFRules(findings) {
|
|
306
|
-
const seen = new Set();
|
|
307
|
-
return findings
|
|
308
|
-
.filter(f => {
|
|
309
|
-
if (seen.has(f.id)) return false;
|
|
310
|
-
seen.add(f.id);
|
|
311
|
-
return true;
|
|
312
|
-
})
|
|
313
|
-
.map(f => ({
|
|
314
|
-
id: f.id,
|
|
315
|
-
shortDescription: { text: f.title },
|
|
316
|
-
fullDescription: { text: f.description || f.title },
|
|
317
|
-
defaultConfiguration: { level: sarifLevel(f.severity) },
|
|
318
|
-
}));
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function sarifLevel(severity) {
|
|
322
|
-
const levels = { critical: "error", high: "error", medium: "warning", low: "note" };
|
|
323
|
-
return levels[severity] || "warning";
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Export to CSV format
|
|
328
|
-
*/
|
|
329
|
-
function exportToCSV(reportData) {
|
|
330
|
-
const headers = ["ID", "Severity", "Category", "Title", "File", "Line", "Fix Time (min)"];
|
|
331
|
-
const rows = reportData.findings.map(f => [
|
|
332
|
-
f.id,
|
|
333
|
-
f.severity,
|
|
334
|
-
f.category,
|
|
335
|
-
`"${(f.title || "").replace(/"/g, '""')}"`,
|
|
336
|
-
f.file || "",
|
|
337
|
-
f.line || "",
|
|
338
|
-
f.fixTime || "",
|
|
339
|
-
]);
|
|
340
|
-
|
|
341
|
-
return [headers.join(","), ...rows.map(r => r.join(","))].join("\n");
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Export to Markdown format
|
|
346
|
-
*/
|
|
347
|
-
function exportToMarkdown(reportData, options = {}) {
|
|
348
|
-
const { meta, summary, findings, fixEstimates, coverage } = reportData;
|
|
349
|
-
const verdictEmoji = summary.verdict === "SHIP" ? "✅" : summary.verdict === "WARN" ? "⚠️" : "🚫";
|
|
350
|
-
|
|
351
|
-
let md = `# ${options.title || "Vibecheck Report"}
|
|
352
|
-
|
|
353
|
-
**Project:** ${meta.projectName}
|
|
354
|
-
**Generated:** ${new Date(meta.generatedAt).toLocaleString()}
|
|
355
|
-
**Report ID:** ${meta.reportId}
|
|
356
|
-
|
|
357
|
-
---
|
|
358
|
-
|
|
359
|
-
## Summary
|
|
360
|
-
|
|
361
|
-
| Metric | Value |
|
|
362
|
-
|--------|-------|
|
|
363
|
-
| **Score** | ${summary.score}/100 |
|
|
364
|
-
| **Verdict** | ${verdictEmoji} ${summary.verdict} |
|
|
365
|
-
| **Total Findings** | ${summary.totalFindings} |
|
|
366
|
-
| **Est. Fix Time** | ${fixEstimates.humanReadable} |
|
|
367
|
-
|
|
368
|
-
### Severity Breakdown
|
|
369
|
-
|
|
370
|
-
| Severity | Count |
|
|
371
|
-
|----------|-------|
|
|
372
|
-
| 🔴 Critical | ${summary.severityCounts.critical} |
|
|
373
|
-
| 🟠 High | ${summary.severityCounts.high} |
|
|
374
|
-
| 🟡 Medium | ${summary.severityCounts.medium} |
|
|
375
|
-
| ⚪ Low | ${summary.severityCounts.low} |
|
|
376
|
-
|
|
377
|
-
### Category Scores
|
|
378
|
-
|
|
379
|
-
| Category | Score |
|
|
380
|
-
|----------|-------|
|
|
381
|
-
${Object.entries(summary.categoryScores).map(([k, v]) => `| ${formatCategoryName(k)} | ${v}% |`).join("\n")}
|
|
382
|
-
|
|
383
|
-
---
|
|
384
|
-
|
|
385
|
-
## Findings
|
|
386
|
-
|
|
387
|
-
`;
|
|
388
|
-
|
|
389
|
-
// Group by severity
|
|
390
|
-
const bySeverity = { critical: [], high: [], medium: [], low: [] };
|
|
391
|
-
for (const f of findings) {
|
|
392
|
-
if (bySeverity[f.severity]) bySeverity[f.severity].push(f);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
for (const [sev, items] of Object.entries(bySeverity)) {
|
|
396
|
-
if (items.length === 0) continue;
|
|
397
|
-
md += `### ${sev.charAt(0).toUpperCase() + sev.slice(1)} (${items.length})\n\n`;
|
|
398
|
-
for (const f of items.slice(0, options.maxFindings || 20)) {
|
|
399
|
-
md += `#### ${f.id}: ${f.title}\n`;
|
|
400
|
-
if (f.file) md += `- **File:** \`${f.file}${f.line ? `:${f.line}` : ""}\`\n`;
|
|
401
|
-
if (f.fix) md += `- **Fix:** ${f.fix}\n`;
|
|
402
|
-
md += `\n`;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
md += `---
|
|
407
|
-
|
|
408
|
-
*Generated by [Vibecheck](https://vibecheck.dev) · ${meta.reportId}*
|
|
409
|
-
`;
|
|
410
|
-
|
|
411
|
-
return md;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Export to JSON format
|
|
416
|
-
*/
|
|
417
|
-
function exportToJSON(reportData) {
|
|
418
|
-
return JSON.stringify(reportData, null, 2);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function formatCategoryName(name) {
|
|
422
|
-
const names = {
|
|
423
|
-
security: "Security",
|
|
424
|
-
auth: "Authentication",
|
|
425
|
-
billing: "Billing/Payments",
|
|
426
|
-
routes: "Route Integrity",
|
|
427
|
-
env: "Environment",
|
|
428
|
-
quality: "Code Quality",
|
|
429
|
-
};
|
|
430
|
-
return names[name] || name.charAt(0).toUpperCase() + name.slice(1);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// ============================================================================
|
|
434
|
-
// EXPORTS
|
|
435
|
-
// ============================================================================
|
|
436
|
-
|
|
437
|
-
module.exports = {
|
|
438
|
-
buildReportData,
|
|
439
|
-
exportToSARIF,
|
|
440
|
-
exportToCSV,
|
|
441
|
-
exportToMarkdown,
|
|
442
|
-
exportToJSON,
|
|
443
|
-
formatCategoryName,
|
|
444
|
-
normalizeSeverity,
|
|
445
|
-
estimateFixTimes,
|
|
446
|
-
generateReportId,
|
|
447
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Report Engine - World-Class Report Generation
|
|
3
|
+
*
|
|
4
|
+
* Enterprise-grade reporting with:
|
|
5
|
+
* - Multiple output formats (HTML, PDF, MD, JSON, SARIF, CSV)
|
|
6
|
+
* - Beautiful interactive HTML with Chart.js
|
|
7
|
+
* - Executive, Technical, Compliance, and Trend reports
|
|
8
|
+
* - Historical analysis and fix time estimates
|
|
9
|
+
* - Print-optimized layouts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// REPORT DATA MODEL
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build comprehensive report data from ship results
|
|
21
|
+
*/
|
|
22
|
+
function buildReportData(shipResults, options = {}) {
|
|
23
|
+
const findings = shipResults?.findings || [];
|
|
24
|
+
const truthpack = shipResults?.truthpack || {};
|
|
25
|
+
|
|
26
|
+
// Severity counts
|
|
27
|
+
const severityCounts = {
|
|
28
|
+
critical: findings.filter(f => f.severity === "BLOCK" || f.severity === "critical").length,
|
|
29
|
+
high: findings.filter(f => f.severity === "high").length,
|
|
30
|
+
medium: findings.filter(f => f.severity === "WARN" || f.severity === "medium").length,
|
|
31
|
+
low: findings.filter(f => f.severity === "low" || f.severity === "INFO").length,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Category scores (invert finding counts to scores)
|
|
35
|
+
const categoryFindings = groupFindingsByCategory(findings);
|
|
36
|
+
const categoryScores = calculateCategoryScores(categoryFindings);
|
|
37
|
+
|
|
38
|
+
// Overall score
|
|
39
|
+
const score = calculateOverallScore(severityCounts, categoryScores);
|
|
40
|
+
|
|
41
|
+
// Verdict
|
|
42
|
+
const verdict = severityCounts.critical > 0 ? "BLOCK" :
|
|
43
|
+
severityCounts.high > 0 || severityCounts.medium > 5 ? "WARN" : "SHIP";
|
|
44
|
+
|
|
45
|
+
// Fix time estimates
|
|
46
|
+
const fixEstimates = estimateFixTimes(findings);
|
|
47
|
+
|
|
48
|
+
// Coverage stats
|
|
49
|
+
const coverage = extractCoverageStats(truthpack);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
meta: {
|
|
53
|
+
projectName: options.projectName || truthpack?.project?.name || path.basename(process.cwd()),
|
|
54
|
+
generatedAt: new Date().toISOString(),
|
|
55
|
+
version: "2.0.0",
|
|
56
|
+
reportId: generateReportId(),
|
|
57
|
+
},
|
|
58
|
+
summary: {
|
|
59
|
+
score,
|
|
60
|
+
verdict,
|
|
61
|
+
totalFindings: findings.length,
|
|
62
|
+
severityCounts,
|
|
63
|
+
categoryScores,
|
|
64
|
+
},
|
|
65
|
+
findings: enrichFindings(findings),
|
|
66
|
+
coverage,
|
|
67
|
+
fixEstimates,
|
|
68
|
+
truthpack: {
|
|
69
|
+
routes: truthpack?.routes?.server?.length || 0,
|
|
70
|
+
envVars: truthpack?.env?.vars?.length || 0,
|
|
71
|
+
hasAuth: !!(truthpack?.auth?.nextMiddleware?.length || truthpack?.auth?.fastify?.hooks?.length),
|
|
72
|
+
hasBilling: !!truthpack?.billing?.webhooks?.length,
|
|
73
|
+
},
|
|
74
|
+
trends: options.includeTrends ? loadHistoricalTrends(options.repoRoot) : null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function groupFindingsByCategory(findings) {
|
|
79
|
+
const groups = {
|
|
80
|
+
security: [],
|
|
81
|
+
auth: [],
|
|
82
|
+
billing: [],
|
|
83
|
+
routes: [],
|
|
84
|
+
env: [],
|
|
85
|
+
quality: [],
|
|
86
|
+
other: [],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
for (const f of findings) {
|
|
90
|
+
const cat = categorizeFindings(f);
|
|
91
|
+
if (groups[cat]) {
|
|
92
|
+
groups[cat].push(f);
|
|
93
|
+
} else {
|
|
94
|
+
groups.other.push(f);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return groups;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function categorizeFindings(finding) {
|
|
102
|
+
const type = (finding.type || finding.id || "").toLowerCase();
|
|
103
|
+
const msg = (finding.message || finding.title || "").toLowerCase();
|
|
104
|
+
|
|
105
|
+
if (type.includes("auth") || msg.includes("auth") || msg.includes("session")) return "auth";
|
|
106
|
+
if (type.includes("billing") || type.includes("stripe") || msg.includes("payment")) return "billing";
|
|
107
|
+
if (type.includes("route") || type.includes("endpoint") || msg.includes("route")) return "routes";
|
|
108
|
+
if (type.includes("env") || msg.includes("environment")) return "env";
|
|
109
|
+
if (type.includes("secret") || type.includes("security") || msg.includes("secret")) return "security";
|
|
110
|
+
if (type.includes("mock") || type.includes("fake") || msg.includes("console")) return "quality";
|
|
111
|
+
return "other";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function calculateCategoryScores(categoryFindings) {
|
|
115
|
+
const scores = {};
|
|
116
|
+
const weights = {
|
|
117
|
+
security: { max: 100, perFinding: 20 },
|
|
118
|
+
auth: { max: 100, perFinding: 15 },
|
|
119
|
+
billing: { max: 100, perFinding: 15 },
|
|
120
|
+
routes: { max: 100, perFinding: 10 },
|
|
121
|
+
env: { max: 100, perFinding: 5 },
|
|
122
|
+
quality: { max: 100, perFinding: 5 },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
for (const [cat, findings] of Object.entries(categoryFindings)) {
|
|
126
|
+
if (cat === "other") continue;
|
|
127
|
+
const w = weights[cat] || { max: 100, perFinding: 10 };
|
|
128
|
+
scores[cat] = Math.max(0, w.max - (findings.length * w.perFinding));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return scores;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function calculateOverallScore(severityCounts, categoryScores) {
|
|
135
|
+
// Weighted average of category scores with severity penalties
|
|
136
|
+
const categoryAvg = Object.values(categoryScores).reduce((a, b) => a + b, 0) /
|
|
137
|
+
Math.max(1, Object.keys(categoryScores).length);
|
|
138
|
+
|
|
139
|
+
// Severity penalties
|
|
140
|
+
const penalties =
|
|
141
|
+
(severityCounts.critical * 25) +
|
|
142
|
+
(severityCounts.high * 10) +
|
|
143
|
+
(severityCounts.medium * 3) +
|
|
144
|
+
(severityCounts.low * 1);
|
|
145
|
+
|
|
146
|
+
return Math.max(0, Math.min(100, Math.round(categoryAvg - penalties)));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function enrichFindings(findings) {
|
|
150
|
+
return findings.map((f, idx) => ({
|
|
151
|
+
id: f.id || `F${String(idx + 1).padStart(3, "0")}`,
|
|
152
|
+
severity: normalizeSeverity(f.severity),
|
|
153
|
+
category: categorizeFindings(f),
|
|
154
|
+
title: f.title || f.message || "Finding",
|
|
155
|
+
description: f.description || f.message || "",
|
|
156
|
+
file: f.file || f.evidence?.file || null,
|
|
157
|
+
line: f.line || f.evidence?.line || null,
|
|
158
|
+
fix: f.fix || f.recommendation || null,
|
|
159
|
+
fixTime: estimateSingleFixTime(f),
|
|
160
|
+
evidence: f.evidence || null,
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeSeverity(sev) {
|
|
165
|
+
const s = String(sev || "medium").toLowerCase();
|
|
166
|
+
if (s === "block" || s === "critical") return "critical";
|
|
167
|
+
if (s === "high") return "high";
|
|
168
|
+
if (s === "warn" || s === "warning" || s === "medium") return "medium";
|
|
169
|
+
return "low";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function estimateFixTimes(findings) {
|
|
173
|
+
const times = {
|
|
174
|
+
critical: { count: 0, totalMinutes: 0 },
|
|
175
|
+
high: { count: 0, totalMinutes: 0 },
|
|
176
|
+
medium: { count: 0, totalMinutes: 0 },
|
|
177
|
+
low: { count: 0, totalMinutes: 0 },
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
for (const f of findings) {
|
|
181
|
+
const sev = normalizeSeverity(f.severity);
|
|
182
|
+
const mins = estimateSingleFixTime(f);
|
|
183
|
+
if (times[sev]) {
|
|
184
|
+
times[sev].count++;
|
|
185
|
+
times[sev].totalMinutes += mins;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const totalMinutes = Object.values(times).reduce((a, t) => a + t.totalMinutes, 0);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
bySeverity: times,
|
|
193
|
+
totalMinutes,
|
|
194
|
+
totalHours: Math.round(totalMinutes / 60 * 10) / 10,
|
|
195
|
+
humanReadable: formatDuration(totalMinutes),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function estimateSingleFixTime(finding) {
|
|
200
|
+
const sev = normalizeSeverity(finding.severity);
|
|
201
|
+
const type = (finding.type || finding.id || "").toLowerCase();
|
|
202
|
+
|
|
203
|
+
// Base times by severity
|
|
204
|
+
const baseTimes = { critical: 60, high: 30, medium: 15, low: 5 };
|
|
205
|
+
let mins = baseTimes[sev] || 15;
|
|
206
|
+
|
|
207
|
+
// Adjustments by type
|
|
208
|
+
if (type.includes("auth") || type.includes("security")) mins *= 1.5;
|
|
209
|
+
if (type.includes("mock") || type.includes("console")) mins *= 0.5;
|
|
210
|
+
|
|
211
|
+
return Math.round(mins);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function formatDuration(minutes) {
|
|
215
|
+
if (minutes < 60) return `${minutes} min`;
|
|
216
|
+
const hours = Math.floor(minutes / 60);
|
|
217
|
+
const mins = minutes % 60;
|
|
218
|
+
if (mins === 0) return `${hours}h`;
|
|
219
|
+
return `${hours}h ${mins}m`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function extractCoverageStats(truthpack) {
|
|
223
|
+
return {
|
|
224
|
+
routes: {
|
|
225
|
+
total: truthpack?.routes?.server?.length || 0,
|
|
226
|
+
protected: truthpack?.routes?.server?.filter(r => r.protected)?.length || 0,
|
|
227
|
+
tested: truthpack?.routes?.coverage?.hit || 0,
|
|
228
|
+
},
|
|
229
|
+
env: {
|
|
230
|
+
total: truthpack?.env?.vars?.length || 0,
|
|
231
|
+
declared: truthpack?.env?.declared?.length || 0,
|
|
232
|
+
},
|
|
233
|
+
auth: {
|
|
234
|
+
middlewareCount: truthpack?.auth?.nextMiddleware?.length || 0,
|
|
235
|
+
patterns: truthpack?.auth?.nextMatcherPatterns?.length || 0,
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function generateReportId() {
|
|
241
|
+
const date = new Date().toISOString().split("T")[0].replace(/-/g, "");
|
|
242
|
+
const rand = Math.random().toString(36).substring(2, 8).toUpperCase();
|
|
243
|
+
return `VC-${date}-${rand}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function loadHistoricalTrends(repoRoot) {
|
|
247
|
+
// Try to load previous reports for trend analysis
|
|
248
|
+
const historyPath = path.join(repoRoot || process.cwd(), ".vibecheck", "history");
|
|
249
|
+
if (!fs.existsSync(historyPath)) return null;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const files = fs.readdirSync(historyPath)
|
|
253
|
+
.filter(f => f.endsWith(".json"))
|
|
254
|
+
.sort()
|
|
255
|
+
.slice(-10);
|
|
256
|
+
|
|
257
|
+
return files.map(f => {
|
|
258
|
+
const data = JSON.parse(fs.readFileSync(path.join(historyPath, f), "utf8"));
|
|
259
|
+
return {
|
|
260
|
+
date: data.meta?.generatedAt || f.replace(".json", ""),
|
|
261
|
+
score: data.summary?.score || 0,
|
|
262
|
+
findings: data.summary?.totalFindings || 0,
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
} catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// EXPORT FORMATS
|
|
272
|
+
// ============================================================================
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Export to SARIF format (Static Analysis Results Interchange Format)
|
|
276
|
+
*/
|
|
277
|
+
function exportToSARIF(reportData) {
|
|
278
|
+
return {
|
|
279
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
280
|
+
version: "2.1.0",
|
|
281
|
+
runs: [{
|
|
282
|
+
tool: {
|
|
283
|
+
driver: {
|
|
284
|
+
name: "vibecheck",
|
|
285
|
+
version: reportData.meta.version,
|
|
286
|
+
informationUri: "https://vibecheck.dev",
|
|
287
|
+
rules: generateSARIFRules(reportData.findings),
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
results: reportData.findings.map(f => ({
|
|
291
|
+
ruleId: f.id,
|
|
292
|
+
level: sarifLevel(f.severity),
|
|
293
|
+
message: { text: f.title },
|
|
294
|
+
locations: f.file ? [{
|
|
295
|
+
physicalLocation: {
|
|
296
|
+
artifactLocation: { uri: f.file },
|
|
297
|
+
region: f.line ? { startLine: f.line } : undefined,
|
|
298
|
+
},
|
|
299
|
+
}] : [],
|
|
300
|
+
})),
|
|
301
|
+
}],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function generateSARIFRules(findings) {
|
|
306
|
+
const seen = new Set();
|
|
307
|
+
return findings
|
|
308
|
+
.filter(f => {
|
|
309
|
+
if (seen.has(f.id)) return false;
|
|
310
|
+
seen.add(f.id);
|
|
311
|
+
return true;
|
|
312
|
+
})
|
|
313
|
+
.map(f => ({
|
|
314
|
+
id: f.id,
|
|
315
|
+
shortDescription: { text: f.title },
|
|
316
|
+
fullDescription: { text: f.description || f.title },
|
|
317
|
+
defaultConfiguration: { level: sarifLevel(f.severity) },
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function sarifLevel(severity) {
|
|
322
|
+
const levels = { critical: "error", high: "error", medium: "warning", low: "note" };
|
|
323
|
+
return levels[severity] || "warning";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Export to CSV format
|
|
328
|
+
*/
|
|
329
|
+
function exportToCSV(reportData) {
|
|
330
|
+
const headers = ["ID", "Severity", "Category", "Title", "File", "Line", "Fix Time (min)"];
|
|
331
|
+
const rows = reportData.findings.map(f => [
|
|
332
|
+
f.id,
|
|
333
|
+
f.severity,
|
|
334
|
+
f.category,
|
|
335
|
+
`"${(f.title || "").replace(/"/g, '""')}"`,
|
|
336
|
+
f.file || "",
|
|
337
|
+
f.line || "",
|
|
338
|
+
f.fixTime || "",
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
return [headers.join(","), ...rows.map(r => r.join(","))].join("\n");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Export to Markdown format
|
|
346
|
+
*/
|
|
347
|
+
function exportToMarkdown(reportData, options = {}) {
|
|
348
|
+
const { meta, summary, findings, fixEstimates, coverage } = reportData;
|
|
349
|
+
const verdictEmoji = summary.verdict === "SHIP" ? "✅" : summary.verdict === "WARN" ? "⚠️" : "🚫";
|
|
350
|
+
|
|
351
|
+
let md = `# ${options.title || "Vibecheck Report"}
|
|
352
|
+
|
|
353
|
+
**Project:** ${meta.projectName}
|
|
354
|
+
**Generated:** ${new Date(meta.generatedAt).toLocaleString()}
|
|
355
|
+
**Report ID:** ${meta.reportId}
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Summary
|
|
360
|
+
|
|
361
|
+
| Metric | Value |
|
|
362
|
+
|--------|-------|
|
|
363
|
+
| **Score** | ${summary.score}/100 |
|
|
364
|
+
| **Verdict** | ${verdictEmoji} ${summary.verdict} |
|
|
365
|
+
| **Total Findings** | ${summary.totalFindings} |
|
|
366
|
+
| **Est. Fix Time** | ${fixEstimates.humanReadable} |
|
|
367
|
+
|
|
368
|
+
### Severity Breakdown
|
|
369
|
+
|
|
370
|
+
| Severity | Count |
|
|
371
|
+
|----------|-------|
|
|
372
|
+
| 🔴 Critical | ${summary.severityCounts.critical} |
|
|
373
|
+
| 🟠 High | ${summary.severityCounts.high} |
|
|
374
|
+
| 🟡 Medium | ${summary.severityCounts.medium} |
|
|
375
|
+
| ⚪ Low | ${summary.severityCounts.low} |
|
|
376
|
+
|
|
377
|
+
### Category Scores
|
|
378
|
+
|
|
379
|
+
| Category | Score |
|
|
380
|
+
|----------|-------|
|
|
381
|
+
${Object.entries(summary.categoryScores).map(([k, v]) => `| ${formatCategoryName(k)} | ${v}% |`).join("\n")}
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Findings
|
|
386
|
+
|
|
387
|
+
`;
|
|
388
|
+
|
|
389
|
+
// Group by severity
|
|
390
|
+
const bySeverity = { critical: [], high: [], medium: [], low: [] };
|
|
391
|
+
for (const f of findings) {
|
|
392
|
+
if (bySeverity[f.severity]) bySeverity[f.severity].push(f);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (const [sev, items] of Object.entries(bySeverity)) {
|
|
396
|
+
if (items.length === 0) continue;
|
|
397
|
+
md += `### ${sev.charAt(0).toUpperCase() + sev.slice(1)} (${items.length})\n\n`;
|
|
398
|
+
for (const f of items.slice(0, options.maxFindings || 20)) {
|
|
399
|
+
md += `#### ${f.id}: ${f.title}\n`;
|
|
400
|
+
if (f.file) md += `- **File:** \`${f.file}${f.line ? `:${f.line}` : ""}\`\n`;
|
|
401
|
+
if (f.fix) md += `- **Fix:** ${f.fix}\n`;
|
|
402
|
+
md += `\n`;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
md += `---
|
|
407
|
+
|
|
408
|
+
*Generated by [Vibecheck](https://vibecheck.dev) · ${meta.reportId}*
|
|
409
|
+
`;
|
|
410
|
+
|
|
411
|
+
return md;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Export to JSON format
|
|
416
|
+
*/
|
|
417
|
+
function exportToJSON(reportData) {
|
|
418
|
+
return JSON.stringify(reportData, null, 2);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function formatCategoryName(name) {
|
|
422
|
+
const names = {
|
|
423
|
+
security: "Security",
|
|
424
|
+
auth: "Authentication",
|
|
425
|
+
billing: "Billing/Payments",
|
|
426
|
+
routes: "Route Integrity",
|
|
427
|
+
env: "Environment",
|
|
428
|
+
quality: "Code Quality",
|
|
429
|
+
};
|
|
430
|
+
return names[name] || name.charAt(0).toUpperCase() + name.slice(1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ============================================================================
|
|
434
|
+
// EXPORTS
|
|
435
|
+
// ============================================================================
|
|
436
|
+
|
|
437
|
+
module.exports = {
|
|
438
|
+
buildReportData,
|
|
439
|
+
exportToSARIF,
|
|
440
|
+
exportToCSV,
|
|
441
|
+
exportToMarkdown,
|
|
442
|
+
exportToJSON,
|
|
443
|
+
formatCategoryName,
|
|
444
|
+
normalizeSeverity,
|
|
445
|
+
estimateFixTimes,
|
|
446
|
+
generateReportId,
|
|
447
|
+
};
|