@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
|
@@ -1,59 +1,99 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Report Engine -
|
|
2
|
+
* Report Engine - Enterprise Data Processing
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Data normalization from multiple scan sources
|
|
6
|
+
* - Trend analysis from historical reports
|
|
7
|
+
* - Export to multiple formats (JSON, SARIF, CSV, Markdown)
|
|
8
|
+
* - Fix time estimation based on severity and type
|
|
9
|
+
* - Category scoring and aggregation
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const fs = require("fs");
|
|
13
13
|
const path = require("path");
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
// Severity weights for scoring
|
|
16
|
+
const SEVERITY_WEIGHTS = {
|
|
17
|
+
critical: 25,
|
|
18
|
+
high: 15,
|
|
19
|
+
medium: 5,
|
|
20
|
+
low: 1,
|
|
21
|
+
info: 0,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Fix time estimates in minutes by severity
|
|
25
|
+
const FIX_TIME_ESTIMATES = {
|
|
26
|
+
critical: { min: 45, max: 120, avg: 60 },
|
|
27
|
+
high: { min: 30, max: 60, avg: 45 },
|
|
28
|
+
medium: { min: 15, max: 30, avg: 20 },
|
|
29
|
+
low: { min: 5, max: 15, avg: 10 },
|
|
30
|
+
info: { min: 2, max: 10, avg: 5 },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Fix time by finding type
|
|
34
|
+
const TYPE_MULTIPLIERS = {
|
|
35
|
+
secret: 1.5, // Requires key rotation
|
|
36
|
+
auth: 2.0, // Complex logic
|
|
37
|
+
billing: 2.5, // Critical + testing
|
|
38
|
+
injection: 1.5,
|
|
39
|
+
xss: 1.2,
|
|
40
|
+
config: 0.8,
|
|
41
|
+
quality: 0.5,
|
|
42
|
+
mock: 0.6,
|
|
43
|
+
error: 0.7,
|
|
44
|
+
};
|
|
18
45
|
|
|
19
46
|
/**
|
|
20
|
-
* Build comprehensive report data from
|
|
47
|
+
* Build comprehensive report data from scan results
|
|
21
48
|
*/
|
|
22
|
-
function buildReportData(shipResults,
|
|
23
|
-
const
|
|
24
|
-
|
|
49
|
+
function buildReportData(shipResults, opts = {}) {
|
|
50
|
+
const { projectName = "Unknown", repoRoot = ".", includeTrends = false } = opts;
|
|
51
|
+
|
|
52
|
+
// Normalize findings
|
|
53
|
+
const rawFindings = shipResults?.findings || [];
|
|
54
|
+
const findings = rawFindings.map((f, i) => normalizeFinding(f, i));
|
|
25
55
|
|
|
26
|
-
//
|
|
56
|
+
// Calculate severity counts
|
|
27
57
|
const severityCounts = {
|
|
28
|
-
critical: findings.filter(f => f.severity === "
|
|
58
|
+
critical: findings.filter(f => f.severity === "critical").length,
|
|
29
59
|
high: findings.filter(f => f.severity === "high").length,
|
|
30
|
-
medium: findings.filter(f => f.severity === "
|
|
31
|
-
low: findings.filter(f => f.severity === "low"
|
|
60
|
+
medium: findings.filter(f => f.severity === "medium").length,
|
|
61
|
+
low: findings.filter(f => f.severity === "low").length,
|
|
62
|
+
info: findings.filter(f => f.severity === "info").length,
|
|
32
63
|
};
|
|
33
64
|
|
|
34
|
-
//
|
|
35
|
-
const
|
|
36
|
-
const categoryScores = calculateCategoryScores(categoryFindings);
|
|
65
|
+
// Calculate score
|
|
66
|
+
const score = calculateScore(severityCounts, findings.length);
|
|
37
67
|
|
|
38
|
-
//
|
|
39
|
-
const
|
|
68
|
+
// Determine verdict
|
|
69
|
+
const verdict = determineVerdict(severityCounts, score);
|
|
40
70
|
|
|
41
|
-
//
|
|
42
|
-
const
|
|
43
|
-
|
|
71
|
+
// Group by category
|
|
72
|
+
const byCategory = groupByCategory(findings);
|
|
73
|
+
const categoryScores = calculateCategoryScores(byCategory);
|
|
44
74
|
|
|
45
|
-
//
|
|
46
|
-
const fixEstimates =
|
|
75
|
+
// Calculate fix estimates
|
|
76
|
+
const fixEstimates = calculateFixEstimates(findings);
|
|
47
77
|
|
|
48
|
-
//
|
|
49
|
-
const
|
|
78
|
+
// Process truthpack
|
|
79
|
+
const truthpack = processTruthpack(shipResults?.truthpack);
|
|
80
|
+
|
|
81
|
+
// Process reality data
|
|
82
|
+
const reality = processReality(shipResults?.reality);
|
|
83
|
+
|
|
84
|
+
// Load trends if requested
|
|
85
|
+
let trends = null;
|
|
86
|
+
if (includeTrends) {
|
|
87
|
+
trends = loadTrends(repoRoot);
|
|
88
|
+
}
|
|
50
89
|
|
|
51
90
|
return {
|
|
52
91
|
meta: {
|
|
53
|
-
projectName
|
|
92
|
+
projectName,
|
|
54
93
|
generatedAt: new Date().toISOString(),
|
|
55
94
|
version: "2.0.0",
|
|
56
95
|
reportId: generateReportId(),
|
|
96
|
+
repoRoot,
|
|
57
97
|
},
|
|
58
98
|
summary: {
|
|
59
99
|
score,
|
|
@@ -61,294 +101,416 @@ function buildReportData(shipResults, options = {}) {
|
|
|
61
101
|
totalFindings: findings.length,
|
|
62
102
|
severityCounts,
|
|
63
103
|
categoryScores,
|
|
104
|
+
topBlockers: findings.filter(f => f.severity === "critical").slice(0, 5),
|
|
64
105
|
},
|
|
65
|
-
findings
|
|
66
|
-
coverage,
|
|
106
|
+
findings,
|
|
67
107
|
fixEstimates,
|
|
68
|
-
truthpack
|
|
69
|
-
|
|
70
|
-
|
|
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,
|
|
108
|
+
truthpack,
|
|
109
|
+
reality,
|
|
110
|
+
trends,
|
|
75
111
|
};
|
|
76
112
|
}
|
|
77
113
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
}
|
|
114
|
+
/**
|
|
115
|
+
* Normalize a finding to consistent format
|
|
116
|
+
*/
|
|
117
|
+
function normalizeFinding(f, index) {
|
|
118
|
+
const severity = normalizeSeverity(f.severity);
|
|
119
|
+
const type = normalizeType(f.type || f.category);
|
|
97
120
|
|
|
98
|
-
return
|
|
121
|
+
return {
|
|
122
|
+
id: f.id || `F${String(index + 1).padStart(3, "0")}`,
|
|
123
|
+
severity,
|
|
124
|
+
type,
|
|
125
|
+
title: f.title || f.message || "Unknown issue",
|
|
126
|
+
message: f.message || f.title || "",
|
|
127
|
+
description: f.description || "",
|
|
128
|
+
file: f.file || f.path || null,
|
|
129
|
+
line: f.line || f.lineNumber || null,
|
|
130
|
+
column: f.column || null,
|
|
131
|
+
fix: f.fix || f.recommendation || f.suggestion || null,
|
|
132
|
+
cwe: f.cwe || null,
|
|
133
|
+
references: f.references || [],
|
|
134
|
+
confidence: f.confidence || "high",
|
|
135
|
+
falsePositive: f.falsePositive || false,
|
|
136
|
+
};
|
|
99
137
|
}
|
|
100
138
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
if (
|
|
108
|
-
if (
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
return "other";
|
|
139
|
+
/**
|
|
140
|
+
* Normalize severity to standard values
|
|
141
|
+
*/
|
|
142
|
+
function normalizeSeverity(sev) {
|
|
143
|
+
const s = String(sev || "").toLowerCase();
|
|
144
|
+
if (s === "block" || s === "critical" || s === "blocker") return "critical";
|
|
145
|
+
if (s === "high" || s === "error") return "high";
|
|
146
|
+
if (s === "warn" || s === "warning" || s === "medium") return "medium";
|
|
147
|
+
if (s === "info" || s === "low" || s === "note") return "low";
|
|
148
|
+
return "info";
|
|
112
149
|
}
|
|
113
150
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Normalize finding type
|
|
153
|
+
*/
|
|
154
|
+
function normalizeType(type) {
|
|
155
|
+
const t = String(type || "").toLowerCase();
|
|
156
|
+
const typeMap = {
|
|
157
|
+
secret: ["secret", "credential", "key", "token", "password", "api_key"],
|
|
158
|
+
auth: ["auth", "authentication", "authorization", "session", "jwt"],
|
|
159
|
+
billing: ["billing", "payment", "stripe", "subscription", "checkout"],
|
|
160
|
+
injection: ["injection", "sql", "nosql", "command", "ldap"],
|
|
161
|
+
xss: ["xss", "cross-site", "script"],
|
|
162
|
+
config: ["config", "configuration", "env", "environment"],
|
|
163
|
+
quality: ["quality", "lint", "style", "format"],
|
|
164
|
+
mock: ["mock", "stub", "fake", "dummy", "placeholder"],
|
|
165
|
+
error: ["error", "exception", "catch", "handling"],
|
|
166
|
+
route: ["route", "endpoint", "path", "url"],
|
|
123
167
|
};
|
|
124
168
|
|
|
125
|
-
for (const [
|
|
126
|
-
if (
|
|
127
|
-
const w = weights[cat] || { max: 100, perFinding: 10 };
|
|
128
|
-
scores[cat] = Math.max(0, w.max - (findings.length * w.perFinding));
|
|
169
|
+
for (const [category, keywords] of Object.entries(typeMap)) {
|
|
170
|
+
if (keywords.some(k => t.includes(k))) return category;
|
|
129
171
|
}
|
|
130
|
-
|
|
131
|
-
return scores;
|
|
172
|
+
return "other";
|
|
132
173
|
}
|
|
133
174
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Calculate overall score
|
|
177
|
+
*/
|
|
178
|
+
function calculateScore(severityCounts, totalFindings) {
|
|
179
|
+
if (totalFindings === 0) return 100;
|
|
138
180
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
(severityCounts.low * 1);
|
|
181
|
+
const deductions =
|
|
182
|
+
severityCounts.critical * SEVERITY_WEIGHTS.critical +
|
|
183
|
+
severityCounts.high * SEVERITY_WEIGHTS.high +
|
|
184
|
+
severityCounts.medium * SEVERITY_WEIGHTS.medium +
|
|
185
|
+
severityCounts.low * SEVERITY_WEIGHTS.low;
|
|
145
186
|
|
|
146
|
-
|
|
187
|
+
// Cap deductions at 100
|
|
188
|
+
const score = Math.max(0, 100 - deductions);
|
|
189
|
+
return Math.round(score);
|
|
147
190
|
}
|
|
148
191
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
}));
|
|
192
|
+
/**
|
|
193
|
+
* Determine verdict from score and findings
|
|
194
|
+
*/
|
|
195
|
+
function determineVerdict(severityCounts, score) {
|
|
196
|
+
if (severityCounts.critical > 0) return "BLOCK";
|
|
197
|
+
if (severityCounts.high > 2 || score < 60) return "BLOCK";
|
|
198
|
+
if (severityCounts.high > 0 || severityCounts.medium > 5 || score < 80) return "WARN";
|
|
199
|
+
return "SHIP";
|
|
162
200
|
}
|
|
163
201
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Group findings by category
|
|
204
|
+
*/
|
|
205
|
+
function groupByCategory(findings) {
|
|
206
|
+
const groups = {};
|
|
207
|
+
for (const f of findings) {
|
|
208
|
+
const cat = f.type || "other";
|
|
209
|
+
if (!groups[cat]) groups[cat] = [];
|
|
210
|
+
groups[cat].push(f);
|
|
211
|
+
}
|
|
212
|
+
return groups;
|
|
170
213
|
}
|
|
171
214
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
low: { count: 0, totalMinutes: 0 },
|
|
178
|
-
};
|
|
215
|
+
/**
|
|
216
|
+
* Calculate category scores
|
|
217
|
+
*/
|
|
218
|
+
function calculateCategoryScores(byCategory) {
|
|
219
|
+
const scores = {};
|
|
179
220
|
|
|
180
|
-
for (const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
221
|
+
for (const [cat, findings] of Object.entries(byCategory)) {
|
|
222
|
+
const deductions = findings.reduce((sum, f) => {
|
|
223
|
+
return sum + (SEVERITY_WEIGHTS[f.severity] || 0);
|
|
224
|
+
}, 0);
|
|
225
|
+
|
|
226
|
+
scores[cat] = Math.max(0, Math.min(100, 100 - deductions));
|
|
187
227
|
}
|
|
188
228
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
bySeverity: times,
|
|
193
|
-
totalMinutes,
|
|
194
|
-
totalHours: Math.round(totalMinutes / 60 * 10) / 10,
|
|
195
|
-
humanReadable: formatDuration(totalMinutes),
|
|
196
|
-
};
|
|
229
|
+
return scores;
|
|
197
230
|
}
|
|
198
231
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Calculate fix time estimates
|
|
234
|
+
*/
|
|
235
|
+
function calculateFixEstimates(findings) {
|
|
236
|
+
let totalMin = 0;
|
|
237
|
+
let totalMax = 0;
|
|
238
|
+
let totalAvg = 0;
|
|
202
239
|
|
|
203
|
-
|
|
204
|
-
const baseTimes = { critical: 60, high: 30, medium: 15, low: 5 };
|
|
205
|
-
let mins = baseTimes[sev] || 15;
|
|
240
|
+
const bySeverity = {};
|
|
206
241
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
242
|
+
for (const f of findings) {
|
|
243
|
+
const sev = f.severity;
|
|
244
|
+
const type = f.type;
|
|
245
|
+
const base = FIX_TIME_ESTIMATES[sev] || FIX_TIME_ESTIMATES.medium;
|
|
246
|
+
const multiplier = TYPE_MULTIPLIERS[type] || 1.0;
|
|
247
|
+
|
|
248
|
+
totalMin += Math.round(base.min * multiplier);
|
|
249
|
+
totalMax += Math.round(base.max * multiplier);
|
|
250
|
+
totalAvg += Math.round(base.avg * multiplier);
|
|
251
|
+
|
|
252
|
+
if (!bySeverity[sev]) bySeverity[sev] = { count: 0, minutes: 0 };
|
|
253
|
+
bySeverity[sev].count++;
|
|
254
|
+
bySeverity[sev].minutes += Math.round(base.avg * multiplier);
|
|
255
|
+
}
|
|
210
256
|
|
|
211
|
-
return
|
|
257
|
+
return {
|
|
258
|
+
totalMinutes: totalAvg,
|
|
259
|
+
range: { min: totalMin, max: totalMax },
|
|
260
|
+
humanReadable: formatDuration(totalAvg),
|
|
261
|
+
rangeReadable: `${formatDuration(totalMin)} - ${formatDuration(totalMax)}`,
|
|
262
|
+
bySeverity,
|
|
263
|
+
};
|
|
212
264
|
}
|
|
213
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Format duration in minutes to human readable
|
|
268
|
+
*/
|
|
214
269
|
function formatDuration(minutes) {
|
|
215
|
-
if (minutes < 60) return `${minutes}
|
|
216
|
-
const hours = Math.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return `${
|
|
270
|
+
if (minutes < 60) return `${minutes}m`;
|
|
271
|
+
const hours = Math.round(minutes / 60 * 10) / 10;
|
|
272
|
+
if (hours < 8) return `${hours}h`;
|
|
273
|
+
const days = Math.round(hours / 8 * 10) / 10;
|
|
274
|
+
return `${days}d`;
|
|
220
275
|
}
|
|
221
276
|
|
|
222
|
-
|
|
277
|
+
/**
|
|
278
|
+
* Process truthpack data
|
|
279
|
+
*/
|
|
280
|
+
function processTruthpack(truthpack) {
|
|
281
|
+
if (!truthpack) return null;
|
|
282
|
+
|
|
223
283
|
return {
|
|
224
|
-
routes:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
middlewareCount: truthpack?.auth?.nextMiddleware?.length || 0,
|
|
235
|
-
patterns: truthpack?.auth?.nextMatcherPatterns?.length || 0,
|
|
284
|
+
routes: Array.isArray(truthpack.routes?.server) ? truthpack.routes.server.length : 0,
|
|
285
|
+
envVars: Array.isArray(truthpack.env?.vars) ? truthpack.env.vars.length : 0,
|
|
286
|
+
hasAuth: !!(truthpack.auth?.nextMiddleware?.length || truthpack.auth?.middleware?.length),
|
|
287
|
+
hasBilling: !!(truthpack.billing?.webhooks?.length || truthpack.billing?.stripeConfig),
|
|
288
|
+
rawCounts: {
|
|
289
|
+
serverRoutes: truthpack.routes?.server?.length || 0,
|
|
290
|
+
clientRoutes: truthpack.routes?.client?.length || 0,
|
|
291
|
+
envVars: truthpack.env?.vars?.length || 0,
|
|
292
|
+
authMiddleware: truthpack.auth?.nextMiddleware?.length || truthpack.auth?.middleware?.length || 0,
|
|
293
|
+
webhooks: truthpack.billing?.webhooks?.length || 0,
|
|
236
294
|
},
|
|
237
295
|
};
|
|
238
296
|
}
|
|
239
297
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
298
|
+
/**
|
|
299
|
+
* Process reality mode data
|
|
300
|
+
*/
|
|
301
|
+
function processReality(reality) {
|
|
302
|
+
if (!reality) return null;
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
coverage: reality.coverage || {},
|
|
306
|
+
brokenFlows: reality.brokenFlows || [],
|
|
307
|
+
unmappedRequests: reality.unmappedRequests || [],
|
|
308
|
+
latencyP95: reality.latencyP95 || null,
|
|
309
|
+
latencySparkline: reality.latencySparkline || [],
|
|
310
|
+
requestsOverTime: reality.requestsOverTime || [],
|
|
311
|
+
totalRequests: reality.totalRequests || 0,
|
|
312
|
+
};
|
|
244
313
|
}
|
|
245
314
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Load historical trends
|
|
317
|
+
*/
|
|
318
|
+
function loadTrends(repoRoot) {
|
|
319
|
+
const historyDir = path.join(repoRoot, ".vibecheck", "history");
|
|
320
|
+
if (!fs.existsSync(historyDir)) return null;
|
|
250
321
|
|
|
251
322
|
try {
|
|
252
|
-
const files = fs.readdirSync(
|
|
323
|
+
const files = fs.readdirSync(historyDir)
|
|
253
324
|
.filter(f => f.endsWith(".json"))
|
|
254
325
|
.sort()
|
|
255
|
-
.slice(-
|
|
326
|
+
.slice(-30); // Last 30 entries
|
|
327
|
+
|
|
328
|
+
const dataPoints = files.map(f => {
|
|
329
|
+
try {
|
|
330
|
+
const data = JSON.parse(fs.readFileSync(path.join(historyDir, f), "utf8"));
|
|
331
|
+
return {
|
|
332
|
+
date: f.replace(".json", ""),
|
|
333
|
+
score: data.summary?.score || 0,
|
|
334
|
+
findings: data.summary?.totalFindings || 0,
|
|
335
|
+
critical: data.summary?.severityCounts?.critical || 0,
|
|
336
|
+
};
|
|
337
|
+
} catch {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}).filter(Boolean);
|
|
341
|
+
|
|
342
|
+
if (dataPoints.length === 0) return null;
|
|
343
|
+
|
|
344
|
+
// Calculate trend
|
|
345
|
+
const scores = dataPoints.map(d => d.score);
|
|
346
|
+
const trend = scores.length >= 2 ? scores[scores.length - 1] - scores[0] : 0;
|
|
256
347
|
|
|
257
|
-
return
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
};
|
|
264
|
-
});
|
|
348
|
+
return {
|
|
349
|
+
dataPoints,
|
|
350
|
+
trend,
|
|
351
|
+
trendDirection: trend > 0 ? "improving" : trend < 0 ? "declining" : "stable",
|
|
352
|
+
averageScore: Math.round(scores.reduce((a, b) => a + b, 0) / scores.length),
|
|
353
|
+
};
|
|
265
354
|
} catch {
|
|
266
355
|
return null;
|
|
267
356
|
}
|
|
268
357
|
}
|
|
269
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Generate unique report ID
|
|
361
|
+
*/
|
|
362
|
+
function generateReportId() {
|
|
363
|
+
const timestamp = Date.now().toString(36).toUpperCase();
|
|
364
|
+
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
|
|
365
|
+
return `VC-${timestamp}-${random}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
270
368
|
// ============================================================================
|
|
271
369
|
// EXPORT FORMATS
|
|
272
370
|
// ============================================================================
|
|
273
371
|
|
|
274
372
|
/**
|
|
275
|
-
* Export to
|
|
373
|
+
* Export to JSON
|
|
374
|
+
*/
|
|
375
|
+
function exportToJSON(reportData) {
|
|
376
|
+
return JSON.stringify(reportData, null, 2);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Export to SARIF format
|
|
276
381
|
*/
|
|
277
382
|
function exportToSARIF(reportData) {
|
|
383
|
+
const rules = new Map();
|
|
384
|
+
const results = [];
|
|
385
|
+
|
|
386
|
+
for (const f of reportData.findings) {
|
|
387
|
+
// Build rule if not exists
|
|
388
|
+
if (!rules.has(f.id)) {
|
|
389
|
+
rules.set(f.id, {
|
|
390
|
+
id: f.id,
|
|
391
|
+
name: f.type ? `${f.type}/${f.id}` : f.id,
|
|
392
|
+
shortDescription: { text: f.title },
|
|
393
|
+
fullDescription: { text: f.description || f.message },
|
|
394
|
+
defaultConfiguration: {
|
|
395
|
+
level: sarifLevel(f.severity),
|
|
396
|
+
},
|
|
397
|
+
helpUri: f.references?.[0] || "https://vibecheck.dev/docs",
|
|
398
|
+
properties: {
|
|
399
|
+
category: f.type,
|
|
400
|
+
cwe: f.cwe,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Build result
|
|
406
|
+
const result = {
|
|
407
|
+
ruleId: f.id,
|
|
408
|
+
level: sarifLevel(f.severity),
|
|
409
|
+
message: { text: f.message || f.title },
|
|
410
|
+
locations: [],
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
if (f.file) {
|
|
414
|
+
result.locations.push({
|
|
415
|
+
physicalLocation: {
|
|
416
|
+
artifactLocation: { uri: f.file },
|
|
417
|
+
region: f.line ? { startLine: f.line, startColumn: f.column || 1 } : undefined,
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (f.fix) {
|
|
423
|
+
result.fixes = [{
|
|
424
|
+
description: { text: f.fix },
|
|
425
|
+
}];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
results.push(result);
|
|
429
|
+
}
|
|
430
|
+
|
|
278
431
|
return {
|
|
279
432
|
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
280
433
|
version: "2.1.0",
|
|
281
434
|
runs: [{
|
|
282
435
|
tool: {
|
|
283
436
|
driver: {
|
|
284
|
-
name: "
|
|
437
|
+
name: "VibeCheck",
|
|
285
438
|
version: reportData.meta.version,
|
|
286
439
|
informationUri: "https://vibecheck.dev",
|
|
287
|
-
rules:
|
|
440
|
+
rules: Array.from(rules.values()),
|
|
288
441
|
},
|
|
289
442
|
},
|
|
290
|
-
results
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
physicalLocation: {
|
|
296
|
-
artifactLocation: { uri: f.file },
|
|
297
|
-
region: f.line ? { startLine: f.line } : undefined,
|
|
298
|
-
},
|
|
299
|
-
}] : [],
|
|
300
|
-
})),
|
|
443
|
+
results,
|
|
444
|
+
invocations: [{
|
|
445
|
+
executionSuccessful: true,
|
|
446
|
+
endTimeUtc: reportData.meta.generatedAt,
|
|
447
|
+
}],
|
|
301
448
|
}],
|
|
302
449
|
};
|
|
303
450
|
}
|
|
304
451
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
452
|
+
/**
|
|
453
|
+
* Convert severity to SARIF level
|
|
454
|
+
*/
|
|
321
455
|
function sarifLevel(severity) {
|
|
322
|
-
const levels = { critical: "error", high: "error", medium: "warning", low: "note" };
|
|
456
|
+
const levels = { critical: "error", high: "error", medium: "warning", low: "note", info: "none" };
|
|
323
457
|
return levels[severity] || "warning";
|
|
324
458
|
}
|
|
325
459
|
|
|
326
460
|
/**
|
|
327
|
-
* Export to CSV
|
|
461
|
+
* Export to CSV
|
|
328
462
|
*/
|
|
329
463
|
function exportToCSV(reportData) {
|
|
330
|
-
const headers = [
|
|
464
|
+
const headers = [
|
|
465
|
+
"ID",
|
|
466
|
+
"Severity",
|
|
467
|
+
"Type",
|
|
468
|
+
"Title",
|
|
469
|
+
"File",
|
|
470
|
+
"Line",
|
|
471
|
+
"Fix",
|
|
472
|
+
"CWE",
|
|
473
|
+
"Confidence",
|
|
474
|
+
];
|
|
475
|
+
|
|
331
476
|
const rows = reportData.findings.map(f => [
|
|
332
477
|
f.id,
|
|
333
478
|
f.severity,
|
|
334
|
-
f.
|
|
335
|
-
|
|
479
|
+
f.type,
|
|
480
|
+
csvEscape(f.title),
|
|
336
481
|
f.file || "",
|
|
337
482
|
f.line || "",
|
|
338
|
-
f.
|
|
483
|
+
csvEscape(f.fix || ""),
|
|
484
|
+
f.cwe || "",
|
|
485
|
+
f.confidence,
|
|
339
486
|
]);
|
|
340
487
|
|
|
341
|
-
return [
|
|
488
|
+
return [
|
|
489
|
+
headers.join(","),
|
|
490
|
+
...rows.map(r => r.join(",")),
|
|
491
|
+
].join("\n");
|
|
342
492
|
}
|
|
343
493
|
|
|
344
494
|
/**
|
|
345
|
-
*
|
|
495
|
+
* Escape CSV value
|
|
346
496
|
*/
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
const
|
|
497
|
+
function csvEscape(val) {
|
|
498
|
+
if (!val) return "";
|
|
499
|
+
const str = String(val);
|
|
500
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
|
|
501
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
502
|
+
}
|
|
503
|
+
return str;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Export to Markdown
|
|
508
|
+
*/
|
|
509
|
+
function exportToMarkdown(reportData, opts = {}) {
|
|
510
|
+
const { meta, summary, findings, fixEstimates, reality } = reportData;
|
|
511
|
+
const redactPaths = opts.redactPaths || false;
|
|
350
512
|
|
|
351
|
-
let md = `#
|
|
513
|
+
let md = `# VibeCheck Report
|
|
352
514
|
|
|
353
515
|
**Project:** ${meta.projectName}
|
|
354
516
|
**Generated:** ${new Date(meta.generatedAt).toLocaleString()}
|
|
@@ -360,25 +522,14 @@ function exportToMarkdown(reportData, options = {}) {
|
|
|
360
522
|
|
|
361
523
|
| Metric | Value |
|
|
362
524
|
|--------|-------|
|
|
363
|
-
|
|
|
364
|
-
|
|
|
365
|
-
|
|
|
366
|
-
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
|
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")}
|
|
525
|
+
| Score | **${summary.score}/100** |
|
|
526
|
+
| Verdict | **${summary.verdict}** |
|
|
527
|
+
| Total Findings | ${summary.totalFindings} |
|
|
528
|
+
| Critical | ${summary.severityCounts.critical} |
|
|
529
|
+
| High | ${summary.severityCounts.high} |
|
|
530
|
+
| Medium | ${summary.severityCounts.medium} |
|
|
531
|
+
| Low | ${summary.severityCounts.low} |
|
|
532
|
+
| Est. Fix Time | ${fixEstimates?.humanReadable || 'N/A'} |
|
|
382
533
|
|
|
383
534
|
---
|
|
384
535
|
|
|
@@ -387,61 +538,89 @@ ${Object.entries(summary.categoryScores).map(([k, v]) => `| ${formatCategoryName
|
|
|
387
538
|
`;
|
|
388
539
|
|
|
389
540
|
// Group by severity
|
|
390
|
-
const bySeverity = {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
541
|
+
const bySeverity = {
|
|
542
|
+
critical: findings.filter(f => f.severity === "critical"),
|
|
543
|
+
high: findings.filter(f => f.severity === "high"),
|
|
544
|
+
medium: findings.filter(f => f.severity === "medium"),
|
|
545
|
+
low: findings.filter(f => f.severity === "low"),
|
|
546
|
+
};
|
|
394
547
|
|
|
395
548
|
for (const [sev, items] of Object.entries(bySeverity)) {
|
|
396
549
|
if (items.length === 0) continue;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
md +=
|
|
550
|
+
|
|
551
|
+
md += `### ${sev.toUpperCase()} (${items.length})\n\n`;
|
|
552
|
+
|
|
553
|
+
for (const f of items.slice(0, 20)) {
|
|
554
|
+
const file = redactPaths && f.file ? redactPath(f.file) : f.file;
|
|
555
|
+
md += `- **${f.title}**\n`;
|
|
556
|
+
if (file) md += ` - File: \`${file}${f.line ? `:${f.line}` : ''}\`\n`;
|
|
557
|
+
if (f.fix) md += ` - Fix: ${f.fix}\n`;
|
|
558
|
+
md += "\n";
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (items.length > 20) {
|
|
562
|
+
md += `_...and ${items.length - 20} more ${sev} findings_\n\n`;
|
|
403
563
|
}
|
|
404
564
|
}
|
|
405
565
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
566
|
+
// Reality section if available
|
|
567
|
+
if (reality && (reality.brokenFlows?.length > 0 || Object.keys(reality.coverage).length > 0)) {
|
|
568
|
+
md += `---\n\n## Reality Mode\n\n`;
|
|
569
|
+
|
|
570
|
+
if (Object.keys(reality.coverage).length > 0) {
|
|
571
|
+
md += `### Coverage\n\n`;
|
|
572
|
+
for (const [key, val] of Object.entries(reality.coverage)) {
|
|
573
|
+
md += `- ${formatCoverageKey(key)}: **${val}%**\n`;
|
|
574
|
+
}
|
|
575
|
+
md += "\n";
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (reality.brokenFlows?.length > 0) {
|
|
579
|
+
md += `### Broken Flows (${reality.brokenFlows.length})\n\n`;
|
|
580
|
+
for (const flow of reality.brokenFlows.slice(0, 5)) {
|
|
581
|
+
md += `- **${flow.title}** (${flow.severity})\n`;
|
|
582
|
+
if (flow.steps) {
|
|
583
|
+
md += ` - Steps: ${flow.steps.map(s => s.label).join(' โ ')}\n`;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
md += "\n";
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
md += `---\n\n*Generated by [VibeCheck](https://vibecheck.dev)*\n`;
|
|
410
591
|
|
|
411
592
|
return md;
|
|
412
593
|
}
|
|
413
594
|
|
|
414
595
|
/**
|
|
415
|
-
*
|
|
596
|
+
* Helper: redact file path
|
|
416
597
|
*/
|
|
417
|
-
function
|
|
418
|
-
|
|
598
|
+
function redactPath(p) {
|
|
599
|
+
if (!p) return p;
|
|
600
|
+
const parts = p.split("/");
|
|
601
|
+
if (parts.length <= 2) return p;
|
|
602
|
+
return ".../" + parts.slice(-2).join("/");
|
|
419
603
|
}
|
|
420
604
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
};
|
|
430
|
-
return names[name] || name.charAt(0).toUpperCase() + name.slice(1);
|
|
605
|
+
/**
|
|
606
|
+
* Helper: format coverage key
|
|
607
|
+
*/
|
|
608
|
+
function formatCoverageKey(key) {
|
|
609
|
+
return key
|
|
610
|
+
.replace(/([A-Z])/g, " $1")
|
|
611
|
+
.replace(/^./, s => s.toUpperCase())
|
|
612
|
+
.trim();
|
|
431
613
|
}
|
|
432
614
|
|
|
433
|
-
// ============================================================================
|
|
434
|
-
// EXPORTS
|
|
435
|
-
// ============================================================================
|
|
436
|
-
|
|
437
615
|
module.exports = {
|
|
438
616
|
buildReportData,
|
|
617
|
+
exportToJSON,
|
|
439
618
|
exportToSARIF,
|
|
440
619
|
exportToCSV,
|
|
441
620
|
exportToMarkdown,
|
|
442
|
-
|
|
443
|
-
|
|
621
|
+
// Expose helpers for testing
|
|
622
|
+
normalizeFinding,
|
|
444
623
|
normalizeSeverity,
|
|
445
|
-
|
|
446
|
-
|
|
624
|
+
calculateScore,
|
|
625
|
+
calculateFixEstimates,
|
|
447
626
|
};
|