@vibecheckai/cli 3.2.0 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/bin/runners/lib/agent-firewall/change-packet/builder.js +214 -0
  2. package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
  3. package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
  4. package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
  5. package/bin/runners/lib/agent-firewall/claims/extractor.js +214 -0
  6. package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
  7. package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
  8. package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
  9. package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +118 -0
  10. package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
  11. package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +142 -0
  12. package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
  13. package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
  14. package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
  15. package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
  16. package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
  17. package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
  18. package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
  19. package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
  20. package/bin/runners/lib/agent-firewall/interceptor/base.js +304 -0
  21. package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
  22. package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
  23. package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
  24. package/bin/runners/lib/agent-firewall/policy/default-policy.json +84 -0
  25. package/bin/runners/lib/agent-firewall/policy/engine.js +72 -0
  26. package/bin/runners/lib/agent-firewall/policy/loader.js +143 -0
  27. package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
  28. package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
  29. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +61 -0
  30. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +50 -0
  31. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +50 -0
  32. package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
  33. package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
  34. package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
  35. package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
  36. package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
  37. package/bin/runners/lib/agent-firewall/truthpack/loader.js +116 -0
  38. package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
  39. package/bin/runners/lib/analysis-core.js +198 -180
  40. package/bin/runners/lib/analyzers.js +1119 -536
  41. package/bin/runners/lib/cli-output.js +236 -210
  42. package/bin/runners/lib/detectors-v2.js +547 -785
  43. package/bin/runners/lib/fingerprint.js +377 -0
  44. package/bin/runners/lib/route-truth.js +1167 -322
  45. package/bin/runners/lib/scan-output.js +144 -738
  46. package/bin/runners/lib/ship-output-enterprise.js +239 -0
  47. package/bin/runners/lib/terminal-ui.js +188 -770
  48. package/bin/runners/lib/truth.js +1004 -321
  49. package/bin/runners/lib/unified-output.js +162 -158
  50. package/bin/runners/runAgent.js +161 -0
  51. package/bin/runners/runFirewall.js +134 -0
  52. package/bin/runners/runFirewallHook.js +56 -0
  53. package/bin/runners/runScan.js +113 -10
  54. package/bin/runners/runShip.js +7 -8
  55. package/bin/runners/runTruth.js +89 -0
  56. package/mcp-server/agent-firewall-interceptor.js +164 -0
  57. package/mcp-server/index.js +347 -313
  58. package/mcp-server/truth-context.js +131 -90
  59. package/mcp-server/truth-firewall-tools.js +1412 -1045
  60. package/package.json +1 -1
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Finding Fingerprint System
3
+ *
4
+ * Provides stable, content-aware fingerprints for findings that allow:
5
+ * - De-duplication across runs without a database
6
+ * - Detection of NEW / FIXED / PERSISTING findings
7
+ * - Comparison even when line numbers shift
8
+ *
9
+ * Fingerprint = hash of: ruleId + file + contextHash (code around the finding)
10
+ */
11
+
12
+ "use strict";
13
+
14
+ const fs = require("fs");
15
+ const path = require("path");
16
+ const crypto = require("crypto");
17
+
18
+ // ═══════════════════════════════════════════════════════════════════════════════
19
+ // FINGERPRINT GENERATION
20
+ // ═══════════════════════════════════════════════════════════════════════════════
21
+
22
+ /**
23
+ * Generate a stable fingerprint for a finding.
24
+ *
25
+ * The fingerprint is resilient to line number changes by:
26
+ * 1. Using a context window of code around the finding
27
+ * 2. Normalizing whitespace in the context
28
+ * 3. Including rule ID and file path
29
+ *
30
+ * @param {Object} finding - The finding object
31
+ * @param {string} repoRoot - Repository root path
32
+ * @returns {string} - Stable fingerprint hash
33
+ */
34
+ function generateFingerprint(finding, repoRoot) {
35
+ const components = [];
36
+
37
+ // 1. Rule ID (category + severity)
38
+ const ruleId = `${finding.category || "unknown"}:${finding.severity || "unknown"}`;
39
+ components.push(ruleId);
40
+
41
+ // 2. File path (relative, normalized)
42
+ const file = extractFile(finding);
43
+ if (file) {
44
+ const relPath = file.startsWith("/") || file.includes(":\\")
45
+ ? path.relative(repoRoot, file).replace(/\\/g, "/")
46
+ : file.replace(/\\/g, "/");
47
+ components.push(relPath);
48
+ }
49
+
50
+ // 3. Context hash (code around the finding)
51
+ const contextHash = getContextHash(finding, repoRoot);
52
+ if (contextHash) {
53
+ components.push(contextHash);
54
+ }
55
+
56
+ // 4. Title/message for uniqueness (normalized)
57
+ const titleNorm = normalizeMessage(finding.title || finding.message || "");
58
+ if (titleNorm) {
59
+ components.push(titleNorm.slice(0, 100)); // Cap to prevent huge fingerprints
60
+ }
61
+
62
+ // Generate hash
63
+ const input = components.join("|");
64
+ return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16);
65
+ }
66
+
67
+ /**
68
+ * Extract file path from finding (handles various formats)
69
+ */
70
+ function extractFile(finding) {
71
+ if (finding.file) return finding.file;
72
+ if (finding.location?.file) return finding.location.file;
73
+ if (finding.evidence?.[0]?.file) return finding.evidence[0].file;
74
+
75
+ // Try to extract from title/message
76
+ const titleMatch = (finding.title || "").match(/:\s*([^\s:]+\.(ts|tsx|js|jsx|json))/);
77
+ if (titleMatch) return titleMatch[1];
78
+
79
+ return null;
80
+ }
81
+
82
+ /**
83
+ * Extract line range from finding
84
+ */
85
+ function extractLines(finding) {
86
+ if (finding.lines) {
87
+ const match = String(finding.lines).match(/(\d+)(?:-(\d+))?/);
88
+ if (match) {
89
+ return { start: parseInt(match[1]), end: parseInt(match[2] || match[1]) };
90
+ }
91
+ }
92
+ if (finding.location?.line) {
93
+ return { start: finding.location.line, end: finding.location.endLine || finding.location.line };
94
+ }
95
+ if (finding.evidence?.[0]?.lines) {
96
+ const match = String(finding.evidence[0].lines).match(/(\d+)(?:-(\d+))?/);
97
+ if (match) {
98
+ return { start: parseInt(match[1]), end: parseInt(match[2] || match[1]) };
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Get a hash of the code context around the finding.
106
+ * Uses a window of ±3 lines to be resilient to minor edits.
107
+ */
108
+ function getContextHash(finding, repoRoot) {
109
+ const file = extractFile(finding);
110
+ const lines = extractLines(finding);
111
+
112
+ if (!file || !lines) {
113
+ // Fall back to snippet hash if available
114
+ if (finding.evidence?.[0]?.snippetHash) {
115
+ return finding.evidence[0].snippetHash.slice(0, 12);
116
+ }
117
+ return null;
118
+ }
119
+
120
+ try {
121
+ const absPath = path.isAbsolute(file) ? file : path.join(repoRoot, file);
122
+ if (!fs.existsSync(absPath)) return null;
123
+
124
+ const content = fs.readFileSync(absPath, "utf8");
125
+ const allLines = content.split(/\r?\n/);
126
+
127
+ // Get context window (±3 lines)
128
+ const contextStart = Math.max(0, lines.start - 4);
129
+ const contextEnd = Math.min(allLines.length, lines.end + 3);
130
+
131
+ const context = allLines
132
+ .slice(contextStart, contextEnd)
133
+ .map(line => line.trim()) // Normalize whitespace
134
+ .filter(line => line.length > 0) // Skip empty lines
135
+ .join("\n");
136
+
137
+ return crypto.createHash("sha256").update(context).digest("hex").slice(0, 12);
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Normalize message for comparison (strip variable parts)
145
+ */
146
+ function normalizeMessage(msg) {
147
+ return msg
148
+ .replace(/:\d+(-\d+)?/g, "") // Remove line numbers
149
+ .replace(/\([^)]*\)/g, "") // Remove parenthetical notes
150
+ .replace(/["'`][^"'`]+["'`]/g, "X") // Replace quoted strings
151
+ .replace(/\s+/g, " ") // Normalize whitespace
152
+ .trim()
153
+ .toLowerCase();
154
+ }
155
+
156
+ // ═══════════════════════════════════════════════════════════════════════════════
157
+ // BASELINE MANAGEMENT
158
+ // ═══════════════════════════════════════════════════════════════════════════════
159
+
160
+ const BASELINE_FILE = ".vibecheck/baseline.json";
161
+
162
+ /**
163
+ * Load baseline from disk
164
+ */
165
+ function loadBaseline(repoRoot) {
166
+ const baselinePath = path.join(repoRoot, BASELINE_FILE);
167
+
168
+ try {
169
+ if (!fs.existsSync(baselinePath)) {
170
+ return { fingerprints: new Map(), timestamp: null, version: "1.0.0" };
171
+ }
172
+
173
+ const data = JSON.parse(fs.readFileSync(baselinePath, "utf8"));
174
+
175
+ return {
176
+ fingerprints: new Map(Object.entries(data.fingerprints || {})),
177
+ timestamp: data.timestamp,
178
+ version: data.version || "1.0.0",
179
+ metadata: data.metadata || {},
180
+ };
181
+ } catch {
182
+ return { fingerprints: new Map(), timestamp: null, version: "1.0.0" };
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Save baseline to disk
188
+ */
189
+ function saveBaseline(repoRoot, findings, metadata = {}) {
190
+ const baselinePath = path.join(repoRoot, BASELINE_FILE);
191
+
192
+ // Ensure directory exists
193
+ const dir = path.dirname(baselinePath);
194
+ if (!fs.existsSync(dir)) {
195
+ fs.mkdirSync(dir, { recursive: true });
196
+ }
197
+
198
+ // Build fingerprint map
199
+ const fingerprints = {};
200
+ for (const finding of findings) {
201
+ if (finding.fingerprint) {
202
+ fingerprints[finding.fingerprint] = {
203
+ category: finding.category,
204
+ severity: finding.severity,
205
+ file: extractFile(finding),
206
+ title: (finding.title || finding.message || "").slice(0, 200),
207
+ firstSeen: finding.firstSeen || new Date().toISOString(),
208
+ };
209
+ }
210
+ }
211
+
212
+ const data = {
213
+ version: "1.0.0",
214
+ timestamp: new Date().toISOString(),
215
+ metadata: {
216
+ totalFindings: findings.length,
217
+ criticalCount: findings.filter(f => f.severity === "BLOCK" || f.severity === "critical").length,
218
+ ...metadata,
219
+ },
220
+ fingerprints,
221
+ };
222
+
223
+ fs.writeFileSync(baselinePath, JSON.stringify(data, null, 2));
224
+
225
+ return data;
226
+ }
227
+
228
+ // ═══════════════════════════════════════════════════════════════════════════════
229
+ // DIFF CALCULATION
230
+ // ═══════════════════════════════════════════════════════════════════════════════
231
+
232
+ /**
233
+ * Compare current findings against baseline to determine status.
234
+ *
235
+ * @param {Array} findings - Current findings (with fingerprints)
236
+ * @param {Object} baseline - Loaded baseline
237
+ * @returns {Object} - Diff result with NEW, FIXED, PERSISTING counts
238
+ */
239
+ function diffFindings(findings, baseline) {
240
+ const currentFP = new Set(findings.map(f => f.fingerprint).filter(Boolean));
241
+ const baselineFP = baseline.fingerprints;
242
+
243
+ const result = {
244
+ new: [],
245
+ fixed: [],
246
+ persisting: [],
247
+ summary: {
248
+ newCount: 0,
249
+ fixedCount: 0,
250
+ persistingCount: 0,
251
+ totalCurrent: findings.length,
252
+ totalBaseline: baselineFP.size,
253
+ },
254
+ };
255
+
256
+ // Find NEW and PERSISTING
257
+ for (const finding of findings) {
258
+ if (!finding.fingerprint) continue;
259
+
260
+ if (baselineFP.has(finding.fingerprint)) {
261
+ finding.status = "PERSISTING";
262
+ finding.firstSeen = baselineFP.get(finding.fingerprint).firstSeen;
263
+ result.persisting.push(finding);
264
+ } else {
265
+ finding.status = "NEW";
266
+ finding.firstSeen = new Date().toISOString();
267
+ result.new.push(finding);
268
+ }
269
+ }
270
+
271
+ // Find FIXED (in baseline but not in current)
272
+ for (const [fp, meta] of baselineFP) {
273
+ if (!currentFP.has(fp)) {
274
+ result.fixed.push({
275
+ fingerprint: fp,
276
+ status: "FIXED",
277
+ ...meta,
278
+ });
279
+ }
280
+ }
281
+
282
+ // Update counts
283
+ result.summary.newCount = result.new.length;
284
+ result.summary.fixedCount = result.fixed.length;
285
+ result.summary.persistingCount = result.persisting.length;
286
+
287
+ return result;
288
+ }
289
+
290
+ // ═══════════════════════════════════════════════════════════════════════════════
291
+ // ENRICHMENT
292
+ // ═══════════════════════════════════════════════════════════════════════════════
293
+
294
+ /**
295
+ * Enrich findings with fingerprints and diff status.
296
+ *
297
+ * @param {Array} findings - Raw findings from analyzers
298
+ * @param {string} repoRoot - Repository root
299
+ * @param {boolean} compareBaseline - Whether to compare against baseline
300
+ * @returns {Object} - { findings, diff, baseline }
301
+ */
302
+ function enrichFindings(findings, repoRoot, compareBaseline = true) {
303
+ // Add fingerprints to all findings
304
+ for (const finding of findings) {
305
+ finding.fingerprint = generateFingerprint(finding, repoRoot);
306
+ }
307
+
308
+ // Load baseline and diff if requested
309
+ let diff = null;
310
+ let baseline = null;
311
+
312
+ if (compareBaseline) {
313
+ baseline = loadBaseline(repoRoot);
314
+
315
+ if (baseline.fingerprints.size > 0) {
316
+ diff = diffFindings(findings, baseline);
317
+ } else {
318
+ // First run - all findings are "new" but we don't show the label
319
+ for (const finding of findings) {
320
+ finding.status = null; // No status on first run
321
+ }
322
+ }
323
+ }
324
+
325
+ return { findings, diff, baseline };
326
+ }
327
+
328
+ /**
329
+ * Format diff summary for terminal output
330
+ */
331
+ function formatDiffSummary(diff) {
332
+ if (!diff) return null;
333
+
334
+ const parts = [];
335
+
336
+ if (diff.summary.newCount > 0) {
337
+ parts.push(`\x1b[38;2;255;100;100m+${diff.summary.newCount} NEW\x1b[0m`);
338
+ }
339
+
340
+ if (diff.summary.fixedCount > 0) {
341
+ parts.push(`\x1b[38;2;100;255;150m-${diff.summary.fixedCount} FIXED\x1b[0m`);
342
+ }
343
+
344
+ if (diff.summary.persistingCount > 0) {
345
+ parts.push(`\x1b[38;2;200;200;200m${diff.summary.persistingCount} persisting\x1b[0m`);
346
+ }
347
+
348
+ return parts.length > 0 ? parts.join(" ") : null;
349
+ }
350
+
351
+ /**
352
+ * Get status badge for a finding
353
+ */
354
+ function getStatusBadge(finding) {
355
+ switch (finding.status) {
356
+ case "NEW":
357
+ return "\x1b[48;2;255;80;80m\x1b[1m NEW \x1b[0m";
358
+ case "FIXED":
359
+ return "\x1b[48;2;50;180;100m\x1b[1m FIXED \x1b[0m";
360
+ case "PERSISTING":
361
+ return "\x1b[38;2;150;150;150m●\x1b[0m"; // Subtle dot for persisting
362
+ default:
363
+ return "";
364
+ }
365
+ }
366
+
367
+ module.exports = {
368
+ generateFingerprint,
369
+ loadBaseline,
370
+ saveBaseline,
371
+ diffFindings,
372
+ enrichFindings,
373
+ formatDiffSummary,
374
+ getStatusBadge,
375
+ extractFile,
376
+ extractLines,
377
+ };