@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,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extracts "drift-causing claims" from changed files.
|
|
5
|
+
* Uses AST-based extraction with Babel parser.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const parser = require("@babel/parser");
|
|
11
|
+
const traverse = require("@babel/traverse").default;
|
|
12
|
+
const t = require("@babel/types");
|
|
13
|
+
const { CLAIM_TYPES, CRITICALITY } = require("./claim-types");
|
|
14
|
+
const { ROUTE_LIKE, AUTH_HINTS, SUCCESS_UI } = require("./patterns");
|
|
15
|
+
|
|
16
|
+
function parse(code) {
|
|
17
|
+
return parser.parse(code, {
|
|
18
|
+
sourceType: "unambiguous",
|
|
19
|
+
plugins: ["typescript", "jsx"]
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function locPtr(fileRel, node) {
|
|
24
|
+
const loc = node && node.loc;
|
|
25
|
+
if (!loc) return null;
|
|
26
|
+
return `${fileRel}:${loc.start.line}-${loc.end.line}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function pushClaim(out, claim) {
|
|
30
|
+
const key = `${claim.type}|${claim.value}|${claim.pointer || ""}`;
|
|
31
|
+
if (!out._dedupe.has(key)) {
|
|
32
|
+
out._dedupe.add(key);
|
|
33
|
+
out.claims.push(claim);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function extractStringLiterals(code) {
|
|
38
|
+
// Cheap scan for route-ish strings even if AST misses template parts.
|
|
39
|
+
const hits = [];
|
|
40
|
+
for (const rx of ROUTE_LIKE) {
|
|
41
|
+
const m = code.match(rx);
|
|
42
|
+
if (m) hits.push(...m);
|
|
43
|
+
}
|
|
44
|
+
return [...new Set(hits)];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function classifyFileDomain(fileRel) {
|
|
48
|
+
const s = fileRel.toLowerCase();
|
|
49
|
+
if (s.includes("auth")) return "auth";
|
|
50
|
+
if (s.includes("stripe") || s.includes("payment")) return "payments";
|
|
51
|
+
if (s.includes("routes") || s.includes("router") || s.includes("api")) return "routes";
|
|
52
|
+
if (s.includes("schema") || s.includes("contract") || s.includes("openapi")) return "contracts";
|
|
53
|
+
if (s.includes("ui") || s.includes("components") || s.includes("pages")) return "ui";
|
|
54
|
+
return "general";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function extractClaimsFromFile({ repoRoot, fileAbs }) {
|
|
58
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
59
|
+
const code = fs.readFileSync(fileAbs, "utf8");
|
|
60
|
+
const ast = parse(code);
|
|
61
|
+
|
|
62
|
+
const out = { fileRel, domain: classifyFileDomain(fileRel), claims: [], _dedupe: new Set() };
|
|
63
|
+
|
|
64
|
+
// 1) quick string route-ish scan
|
|
65
|
+
for (const r of extractStringLiterals(code)) {
|
|
66
|
+
pushClaim(out, {
|
|
67
|
+
type: CLAIM_TYPES.ROUTE,
|
|
68
|
+
value: r,
|
|
69
|
+
criticality: CRITICALITY.HARD,
|
|
70
|
+
pointer: `${fileRel}:1-1`,
|
|
71
|
+
reason: "route-like string literal"
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 2) AST-based env extraction
|
|
76
|
+
traverse(ast, {
|
|
77
|
+
MemberExpression(p) {
|
|
78
|
+
// process.env.X
|
|
79
|
+
const n = p.node;
|
|
80
|
+
if (
|
|
81
|
+
t.isMemberExpression(n.object) &&
|
|
82
|
+
t.isIdentifier(n.object.object, { name: "process" }) &&
|
|
83
|
+
t.isIdentifier(n.object.property, { name: "env" }) &&
|
|
84
|
+
(t.isIdentifier(n.property) || t.isStringLiteral(n.property))
|
|
85
|
+
) {
|
|
86
|
+
const name = t.isIdentifier(n.property) ? n.property.name : n.property.value;
|
|
87
|
+
pushClaim(out, {
|
|
88
|
+
type: CLAIM_TYPES.ENV,
|
|
89
|
+
value: name,
|
|
90
|
+
criticality: CRITICALITY.HARD,
|
|
91
|
+
pointer: locPtr(fileRel, n),
|
|
92
|
+
reason: "process.env usage"
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// import.meta.env.X (Vite)
|
|
97
|
+
if (
|
|
98
|
+
t.isMemberExpression(n.object) &&
|
|
99
|
+
t.isMemberExpression(n.object.object) &&
|
|
100
|
+
t.isIdentifier(n.object.object.object, { name: "import" }) &&
|
|
101
|
+
t.isIdentifier(n.object.object.property, { name: "meta" }) &&
|
|
102
|
+
t.isIdentifier(n.object.property, { name: "env" }) &&
|
|
103
|
+
(t.isIdentifier(n.property) || t.isStringLiteral(n.property))
|
|
104
|
+
) {
|
|
105
|
+
const name = t.isIdentifier(n.property) ? n.property.name : n.property.value;
|
|
106
|
+
pushClaim(out, {
|
|
107
|
+
type: CLAIM_TYPES.ENV,
|
|
108
|
+
value: name,
|
|
109
|
+
criticality: CRITICALITY.HARD,
|
|
110
|
+
pointer: locPtr(fileRel, n),
|
|
111
|
+
reason: "import.meta.env usage"
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
CallExpression(p) {
|
|
117
|
+
const n = p.node;
|
|
118
|
+
|
|
119
|
+
// fetch("/api/...")
|
|
120
|
+
if (t.isIdentifier(n.callee, { name: "fetch" }) && n.arguments[0]) {
|
|
121
|
+
const a0 = n.arguments[0];
|
|
122
|
+
if (t.isStringLiteral(a0)) {
|
|
123
|
+
pushClaim(out, {
|
|
124
|
+
type: CLAIM_TYPES.HTTP_CALL,
|
|
125
|
+
value: a0.value,
|
|
126
|
+
criticality: CRITICALITY.HARD,
|
|
127
|
+
pointer: locPtr(fileRel, n),
|
|
128
|
+
reason: "fetch call"
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// axios.get("/api/...")
|
|
134
|
+
if (t.isMemberExpression(n.callee) && t.isIdentifier(n.callee.object, { name: "axios" })) {
|
|
135
|
+
const method = t.isIdentifier(n.callee.property) ? n.callee.property.name : "call";
|
|
136
|
+
const a0 = n.arguments[0];
|
|
137
|
+
if (a0 && t.isStringLiteral(a0)) {
|
|
138
|
+
pushClaim(out, {
|
|
139
|
+
type: CLAIM_TYPES.HTTP_CALL,
|
|
140
|
+
value: `${method.toUpperCase()} ${a0.value}`,
|
|
141
|
+
criticality: CRITICALITY.HARD,
|
|
142
|
+
pointer: locPtr(fileRel, n),
|
|
143
|
+
reason: "axios call"
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// toast.success("Saved")
|
|
149
|
+
if (
|
|
150
|
+
t.isMemberExpression(n.callee) &&
|
|
151
|
+
t.isIdentifier(n.callee.object, { name: "toast" }) &&
|
|
152
|
+
t.isIdentifier(n.callee.property, { name: "success" })
|
|
153
|
+
) {
|
|
154
|
+
const msg = n.arguments[0] && t.isStringLiteral(n.arguments[0]) ? n.arguments[0].value : "toast.success";
|
|
155
|
+
pushClaim(out, {
|
|
156
|
+
type: CLAIM_TYPES.UI_SUCCESS,
|
|
157
|
+
value: msg,
|
|
158
|
+
criticality: CRITICALITY.SOFT,
|
|
159
|
+
pointer: locPtr(fileRel, n),
|
|
160
|
+
reason: "success UI signal"
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// 3) auth-ish keyword scan (cheap but effective)
|
|
167
|
+
const lines = code.split(/\r?\n/);
|
|
168
|
+
for (let i = 0; i < lines.length; i++) {
|
|
169
|
+
const line = lines[i];
|
|
170
|
+
if (AUTH_HINTS.some((rx) => rx.test(line))) {
|
|
171
|
+
pushClaim(out, {
|
|
172
|
+
type: CLAIM_TYPES.AUTH,
|
|
173
|
+
value: line.trim().slice(0, 140),
|
|
174
|
+
criticality: CRITICALITY.HARD,
|
|
175
|
+
pointer: `${fileRel}:${i + 1}-${i + 1}`,
|
|
176
|
+
reason: "auth/role/scope hint"
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (SUCCESS_UI.some((rx) => rx.test(line))) {
|
|
180
|
+
pushClaim(out, {
|
|
181
|
+
type: CLAIM_TYPES.UI_SUCCESS,
|
|
182
|
+
value: line.trim().slice(0, 140),
|
|
183
|
+
criticality: CRITICALITY.SOFT,
|
|
184
|
+
pointer: `${fileRel}:${i + 1}-${i + 1}`,
|
|
185
|
+
reason: "success UI hint"
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
delete out._dedupe;
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function extractClaims({ repoRoot, changedFilesAbs }) {
|
|
195
|
+
const files = changedFilesAbs.filter((f) => fs.existsSync(f));
|
|
196
|
+
const perFile = files.map((fileAbs) => extractClaimsFromFile({ repoRoot, fileAbs }));
|
|
197
|
+
|
|
198
|
+
// Flatten + dedupe across files
|
|
199
|
+
const dedupe = new Set();
|
|
200
|
+
const claims = [];
|
|
201
|
+
for (const f of perFile) {
|
|
202
|
+
for (const c of f.claims) {
|
|
203
|
+
const k = `${c.type}|${c.value}`;
|
|
204
|
+
if (!dedupe.has(k)) {
|
|
205
|
+
dedupe.add(k);
|
|
206
|
+
claims.push({ ...c, file: f.fileRel, domain: f.domain });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { claims, perFile };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = { extractClaims, extractClaimsFromFile };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim Patterns
|
|
3
|
+
*
|
|
4
|
+
* Regex patterns for detecting claims in code.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const ROUTE_LIKE = [
|
|
8
|
+
/\/api\/[a-zA-Z0-9_\/\-]+/g,
|
|
9
|
+
/\/health\/[a-zA-Z0-9_\/\-]+/g
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const AUTH_HINTS = [
|
|
13
|
+
/\b(admin|owner|staff|superadmin|root)\b/i,
|
|
14
|
+
/\b(role|roles|scope|scopes|permission|permissions)\b/i,
|
|
15
|
+
/\b(auth|authorize|authorization|requireAuth|requireRole|rbac)\b/i
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const SUCCESS_UI = [
|
|
19
|
+
/\b(success|saved|updated|complete|done)\b/i,
|
|
20
|
+
/\btoast\.(success|info)\b/i,
|
|
21
|
+
/\benqueueSnackbar\b/i
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
module.exports = { ROUTE_LIKE, AUTH_HINTS, SUCCESS_UI };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Evidence Resolver
|
|
3
|
+
*
|
|
4
|
+
* Resolves auth claims against truthpack.auth.json
|
|
5
|
+
* Checks for auth drift (claimed restriction not enforced).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const { getAuthRules } = require("../truthpack");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve auth claim evidence
|
|
14
|
+
* @param {string} projectRoot - Project root directory
|
|
15
|
+
* @param {object} claim - Auth claim
|
|
16
|
+
* @returns {object} Evidence result
|
|
17
|
+
*/
|
|
18
|
+
function resolve(projectRoot, claim) {
|
|
19
|
+
const authData = getAuthRules(projectRoot);
|
|
20
|
+
|
|
21
|
+
// Extract auth keywords from claim value
|
|
22
|
+
const claimText = claim.value.toLowerCase();
|
|
23
|
+
const hasAuthKeywords = /\b(admin|owner|staff|role|scope|permission|auth|authorize|rbac)\b/i.test(claimText);
|
|
24
|
+
|
|
25
|
+
if (!hasAuthKeywords) {
|
|
26
|
+
// Not an auth-related claim
|
|
27
|
+
return {
|
|
28
|
+
result: "PROVEN",
|
|
29
|
+
sources: [],
|
|
30
|
+
reason: "No auth keywords detected in claim"
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if auth middleware exists
|
|
35
|
+
const nextMiddleware = authData.nextMiddleware || [];
|
|
36
|
+
const fastifyHooks = authData.fastify?.hooks || [];
|
|
37
|
+
|
|
38
|
+
if (nextMiddleware.length > 0 || fastifyHooks.length > 0) {
|
|
39
|
+
// Auth infrastructure exists
|
|
40
|
+
// Check if claim matches protected patterns
|
|
41
|
+
const matcherPatterns = authData.nextMatcherPatterns || [];
|
|
42
|
+
const claimFile = claim.file || "";
|
|
43
|
+
|
|
44
|
+
// Check if file is in protected path
|
|
45
|
+
const isProtected = matcherPatterns.some(pattern => {
|
|
46
|
+
// Simple pattern matching
|
|
47
|
+
if (pattern.includes("*")) {
|
|
48
|
+
const regex = new RegExp(pattern.replace(/\*/g, ".*"));
|
|
49
|
+
return regex.test(claimFile);
|
|
50
|
+
}
|
|
51
|
+
return claimFile.includes(pattern);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (isProtected) {
|
|
55
|
+
return {
|
|
56
|
+
result: "PROVEN",
|
|
57
|
+
sources: [{
|
|
58
|
+
type: "truthpack.auth",
|
|
59
|
+
pointer: claim.pointer,
|
|
60
|
+
confidence: 0.8
|
|
61
|
+
}],
|
|
62
|
+
reason: "Auth claim matches protected route pattern"
|
|
63
|
+
};
|
|
64
|
+
} else {
|
|
65
|
+
// Auth keywords present but route not protected - potential drift
|
|
66
|
+
return {
|
|
67
|
+
result: "CONTRADICTS",
|
|
68
|
+
sources: [{
|
|
69
|
+
type: "truthpack.auth",
|
|
70
|
+
pointer: claim.pointer,
|
|
71
|
+
confidence: 0.7
|
|
72
|
+
}],
|
|
73
|
+
reason: "Auth keywords present but route not in protected patterns (auth drift)"
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
// No auth infrastructure - cannot verify
|
|
78
|
+
return {
|
|
79
|
+
result: "UNPROVEN",
|
|
80
|
+
sources: [],
|
|
81
|
+
reason: "No auth middleware found in truthpack"
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
resolve
|
|
88
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract Evidence Resolver
|
|
3
|
+
*
|
|
4
|
+
* Resolves contract claims against truthpack.contracts.json
|
|
5
|
+
* Checks for contract drift (API shape mismatch).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const { getContracts } = require("../truthpack");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve contract claim evidence
|
|
14
|
+
* @param {string} projectRoot - Project root directory
|
|
15
|
+
* @param {object} claim - Contract claim
|
|
16
|
+
* @returns {object} Evidence result
|
|
17
|
+
*/
|
|
18
|
+
function resolve(projectRoot, claim) {
|
|
19
|
+
const contracts = getContracts(projectRoot);
|
|
20
|
+
|
|
21
|
+
// Extract contract identifier from claim
|
|
22
|
+
// Contract claims might reference API endpoints, types, or schemas
|
|
23
|
+
const claimValue = claim.value.toLowerCase();
|
|
24
|
+
|
|
25
|
+
// Check if contracts exist
|
|
26
|
+
if (!contracts || Object.keys(contracts).length === 0) {
|
|
27
|
+
return {
|
|
28
|
+
result: "UNPROVEN",
|
|
29
|
+
sources: [],
|
|
30
|
+
reason: "No contracts found in truthpack"
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Try to match claim against contract definitions
|
|
35
|
+
// This is a simplified check - full implementation would parse contract schemas
|
|
36
|
+
const contractKeys = Object.keys(contracts);
|
|
37
|
+
const matchingContract = contractKeys.find(key =>
|
|
38
|
+
key.toLowerCase().includes(claimValue) ||
|
|
39
|
+
claimValue.includes(key.toLowerCase())
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (matchingContract) {
|
|
43
|
+
return {
|
|
44
|
+
result: "PROVEN",
|
|
45
|
+
sources: [{
|
|
46
|
+
type: "truthpack.contracts",
|
|
47
|
+
pointer: claim.pointer,
|
|
48
|
+
confidence: 0.8
|
|
49
|
+
}],
|
|
50
|
+
reason: `Contract ${matchingContract} found in truthpack`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for contract drift by examining the claim context
|
|
55
|
+
// If claim references an API endpoint, check if contract exists for that endpoint
|
|
56
|
+
if (claimValue.includes("api") || claimValue.includes("endpoint")) {
|
|
57
|
+
// Potential contract drift - endpoint referenced but contract not found
|
|
58
|
+
return {
|
|
59
|
+
result: "CONTRADICTS",
|
|
60
|
+
sources: [],
|
|
61
|
+
reason: "API endpoint referenced but contract not found in truthpack (contract drift)"
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Cannot verify contract
|
|
66
|
+
return {
|
|
67
|
+
result: "UNPROVEN",
|
|
68
|
+
sources: [],
|
|
69
|
+
reason: "Contract not found in truthpack"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
resolve
|
|
75
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Variable Evidence Resolver
|
|
3
|
+
*
|
|
4
|
+
* Resolves env var claims against truthpack.env.json
|
|
5
|
+
* Checks for ghost env vars (used but not declared).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const { getEnvVars } = require("../truthpack");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve env var claim evidence
|
|
16
|
+
* @param {string} projectRoot - Project root directory
|
|
17
|
+
* @param {object} claim - Env var claim
|
|
18
|
+
* @returns {object} Evidence result
|
|
19
|
+
*/
|
|
20
|
+
function resolve(projectRoot, claim) {
|
|
21
|
+
const envData = getEnvVars(projectRoot);
|
|
22
|
+
|
|
23
|
+
// Check declared env vars
|
|
24
|
+
const declared = envData.declared || [];
|
|
25
|
+
const declaredSet = new Set(declared.map(v => v.name || v));
|
|
26
|
+
|
|
27
|
+
// Check declared sources (env.schema.ts, .env.example, etc.)
|
|
28
|
+
const declaredSources = envData.declaredSources || [];
|
|
29
|
+
|
|
30
|
+
const envVarName = claim.value;
|
|
31
|
+
|
|
32
|
+
// Check if env var is declared
|
|
33
|
+
if (declaredSet.has(envVarName)) {
|
|
34
|
+
// Find source file
|
|
35
|
+
const source = declaredSources.find(s =>
|
|
36
|
+
s.vars && s.vars.includes(envVarName)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
result: "PROVEN",
|
|
41
|
+
sources: [{
|
|
42
|
+
type: "truthpack.env",
|
|
43
|
+
pointer: source ? source.file : claim.pointer,
|
|
44
|
+
confidence: 0.9
|
|
45
|
+
}],
|
|
46
|
+
reason: `Environment variable ${envVarName} found in truthpack`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if env var exists in .env.example or schema files
|
|
51
|
+
const envExamplePath = path.join(projectRoot, ".env.example");
|
|
52
|
+
const envSchemaPath = findEnvSchemaFile(projectRoot);
|
|
53
|
+
|
|
54
|
+
if (fs.existsSync(envExamplePath)) {
|
|
55
|
+
const envExample = fs.readFileSync(envExamplePath, "utf8");
|
|
56
|
+
if (envExample.includes(envVarName)) {
|
|
57
|
+
return {
|
|
58
|
+
result: "PROVEN",
|
|
59
|
+
sources: [{
|
|
60
|
+
type: "repo.search",
|
|
61
|
+
pointer: ".env.example",
|
|
62
|
+
confidence: 0.7
|
|
63
|
+
}],
|
|
64
|
+
reason: `Environment variable ${envVarName} found in .env.example`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (envSchemaPath && fs.existsSync(envSchemaPath)) {
|
|
70
|
+
const envSchema = fs.readFileSync(envSchemaPath, "utf8");
|
|
71
|
+
if (envSchema.includes(envVarName)) {
|
|
72
|
+
return {
|
|
73
|
+
result: "PROVEN",
|
|
74
|
+
sources: [{
|
|
75
|
+
type: "repo.search",
|
|
76
|
+
pointer: envSchemaPath,
|
|
77
|
+
confidence: 0.8
|
|
78
|
+
}],
|
|
79
|
+
reason: `Environment variable ${envVarName} found in env schema`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Not found - ghost env var
|
|
85
|
+
return {
|
|
86
|
+
result: "UNPROVEN",
|
|
87
|
+
sources: [],
|
|
88
|
+
reason: `Environment variable ${envVarName} not declared (ghost env var)`
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Find env schema file (env.schema.ts, env.ts, etc.)
|
|
94
|
+
* @param {string} projectRoot - Project root directory
|
|
95
|
+
* @returns {string|null} Path to schema file or null
|
|
96
|
+
*/
|
|
97
|
+
function findEnvSchemaFile(projectRoot) {
|
|
98
|
+
const candidates = [
|
|
99
|
+
"apps/api/src/config/env.schema.ts",
|
|
100
|
+
"apps/api/src/env.schema.ts",
|
|
101
|
+
"src/config/env.schema.ts",
|
|
102
|
+
"src/env.schema.ts",
|
|
103
|
+
"env.schema.ts"
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
for (const candidate of candidates) {
|
|
107
|
+
const fullPath = path.join(projectRoot, candidate);
|
|
108
|
+
if (fs.existsSync(fullPath)) {
|
|
109
|
+
return candidate;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
resolve
|
|
118
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence Resolver
|
|
3
|
+
*
|
|
4
|
+
* Main orchestrator for resolving claims against truthpack.
|
|
5
|
+
* Returns PROVEN, UNPROVEN, or CONTRADICTS for each claim.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const routeEvidence = require("./route-evidence");
|
|
11
|
+
const envEvidence = require("./env-evidence");
|
|
12
|
+
const authEvidence = require("./auth-evidence");
|
|
13
|
+
const contractEvidence = require("./contract-evidence");
|
|
14
|
+
const sideEffectEvidence = require("./side-effect-evidence");
|
|
15
|
+
const { CLAIM_TYPES } = require("../claims/claim-types");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve evidence for all claims
|
|
19
|
+
* @param {string} projectRoot - Project root directory
|
|
20
|
+
* @param {array} claims - Array of claims to resolve
|
|
21
|
+
* @returns {array} Array of evidence results
|
|
22
|
+
*/
|
|
23
|
+
function resolveEvidence(projectRoot, claims) {
|
|
24
|
+
const results = [];
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < claims.length; i++) {
|
|
27
|
+
const claim = claims[i];
|
|
28
|
+
const claimId = `claim_${i}`;
|
|
29
|
+
|
|
30
|
+
let result;
|
|
31
|
+
|
|
32
|
+
switch (claim.type) {
|
|
33
|
+
case CLAIM_TYPES.ROUTE:
|
|
34
|
+
result = routeEvidence.resolve(projectRoot, claim);
|
|
35
|
+
break;
|
|
36
|
+
|
|
37
|
+
case CLAIM_TYPES.ENV:
|
|
38
|
+
result = envEvidence.resolve(projectRoot, claim);
|
|
39
|
+
break;
|
|
40
|
+
|
|
41
|
+
case CLAIM_TYPES.AUTH:
|
|
42
|
+
result = authEvidence.resolve(projectRoot, claim);
|
|
43
|
+
break;
|
|
44
|
+
|
|
45
|
+
case CLAIM_TYPES.CONTRACT:
|
|
46
|
+
result = contractEvidence.resolve(projectRoot, claim);
|
|
47
|
+
break;
|
|
48
|
+
|
|
49
|
+
case CLAIM_TYPES.SIDE_EFFECT:
|
|
50
|
+
result = sideEffectEvidence.resolve(projectRoot, claim);
|
|
51
|
+
break;
|
|
52
|
+
|
|
53
|
+
case CLAIM_TYPES.HTTP_CALL:
|
|
54
|
+
// HTTP calls are checked as routes
|
|
55
|
+
result = routeEvidence.resolve(projectRoot, {
|
|
56
|
+
...claim,
|
|
57
|
+
type: CLAIM_TYPES.ROUTE,
|
|
58
|
+
value: extractRouteFromHttpCall(claim.value)
|
|
59
|
+
});
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case CLAIM_TYPES.UI_SUCCESS:
|
|
63
|
+
// UI success claims are checked for side effects
|
|
64
|
+
result = sideEffectEvidence.resolve(projectRoot, claim);
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
default:
|
|
68
|
+
result = {
|
|
69
|
+
claimId,
|
|
70
|
+
result: "UNPROVEN",
|
|
71
|
+
sources: [],
|
|
72
|
+
reason: `Unknown claim type: ${claim.type}`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
results.push({
|
|
77
|
+
claimId,
|
|
78
|
+
...result
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extract route path from HTTP call claim value
|
|
87
|
+
* @param {string} httpCall - HTTP call string (e.g., "GET /api/users")
|
|
88
|
+
* @returns {string} Route path
|
|
89
|
+
*/
|
|
90
|
+
function extractRouteFromHttpCall(httpCall) {
|
|
91
|
+
// Handle "GET /api/users" format
|
|
92
|
+
const match = httpCall.match(/\s+(.+)$/);
|
|
93
|
+
if (match) {
|
|
94
|
+
return match[1];
|
|
95
|
+
}
|
|
96
|
+
// Handle "/api/users" format
|
|
97
|
+
return httpCall.startsWith("/") ? httpCall : `/${httpCall}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
resolveEvidence
|
|
102
|
+
};
|