@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.
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +214 -0
- package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
- package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
- package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +214 -0
- package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
- package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
- package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +118 -0
- package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +142 -0
- package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
- package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
- package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
- package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
- package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
- package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
- package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
- package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
- package/bin/runners/lib/agent-firewall/interceptor/base.js +304 -0
- package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +84 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +72 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +143 -0
- package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +61 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
- package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
- package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
- package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
- package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +116 -0
- package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
- package/bin/runners/lib/analysis-core.js +198 -180
- package/bin/runners/lib/analyzers.js +1119 -536
- package/bin/runners/lib/cli-output.js +236 -210
- package/bin/runners/lib/detectors-v2.js +547 -785
- package/bin/runners/lib/fingerprint.js +377 -0
- package/bin/runners/lib/route-truth.js +1167 -322
- package/bin/runners/lib/scan-output.js +144 -738
- package/bin/runners/lib/ship-output-enterprise.js +239 -0
- package/bin/runners/lib/terminal-ui.js +188 -770
- package/bin/runners/lib/truth.js +1004 -321
- package/bin/runners/lib/unified-output.js +162 -158
- package/bin/runners/runAgent.js +161 -0
- package/bin/runners/runFirewall.js +134 -0
- package/bin/runners/runFirewallHook.js +56 -0
- package/bin/runners/runScan.js +113 -10
- package/bin/runners/runShip.js +7 -8
- package/bin/runners/runTruth.js +89 -0
- package/mcp-server/agent-firewall-interceptor.js +164 -0
- package/mcp-server/index.js +347 -313
- package/mcp-server/truth-context.js +131 -90
- package/mcp-server/truth-firewall-tools.js +1412 -1045
- 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
|
+
};
|