@vibecheckai/cli 3.2.6 → 3.3.0
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/registry.js +192 -5
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
- package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
- package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
- package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
- package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
- package/bin/runners/lib/agent-firewall/logger.js +141 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
- package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
- package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
- package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
- package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
- package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
- package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
- package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
- package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
- package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
- package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
- package/bin/runners/lib/analyzers.js +81 -18
- package/bin/runners/lib/authority-badge.js +425 -0
- package/bin/runners/lib/cli-output.js +7 -1
- package/bin/runners/lib/error-handler.js +16 -9
- package/bin/runners/lib/exit-codes.js +275 -0
- package/bin/runners/lib/global-flags.js +37 -0
- package/bin/runners/lib/help-formatter.js +413 -0
- package/bin/runners/lib/logger.js +38 -0
- package/bin/runners/lib/unified-cli-output.js +604 -0
- package/bin/runners/lib/upsell.js +148 -0
- package/bin/runners/runApprove.js +1200 -0
- package/bin/runners/runAuth.js +324 -95
- package/bin/runners/runCheckpoint.js +39 -21
- package/bin/runners/runClassify.js +859 -0
- package/bin/runners/runContext.js +136 -24
- package/bin/runners/runDoctor.js +108 -68
- package/bin/runners/runFix.js +6 -5
- package/bin/runners/runGuard.js +212 -118
- package/bin/runners/runInit.js +3 -2
- package/bin/runners/runMcp.js +130 -52
- package/bin/runners/runPolish.js +43 -20
- package/bin/runners/runProve.js +1 -2
- package/bin/runners/runReport.js +3 -2
- package/bin/runners/runScan.js +63 -44
- package/bin/runners/runShip.js +3 -4
- package/bin/runners/runValidate.js +19 -2
- package/bin/runners/runWatch.js +104 -53
- package/bin/vibecheck.js +106 -19
- package/mcp-server/HARDENING_SUMMARY.md +299 -0
- package/mcp-server/agent-firewall-interceptor.js +367 -31
- package/mcp-server/authority-tools.js +569 -0
- package/mcp-server/conductor/conflict-resolver.js +588 -0
- package/mcp-server/conductor/execution-planner.js +544 -0
- package/mcp-server/conductor/index.js +377 -0
- package/mcp-server/conductor/lock-manager.js +615 -0
- package/mcp-server/conductor/request-queue.js +550 -0
- package/mcp-server/conductor/session-manager.js +500 -0
- package/mcp-server/conductor/tools.js +510 -0
- package/mcp-server/index.js +1149 -243
- package/mcp-server/lib/{api-client.js → api-client.cjs} +40 -4
- package/mcp-server/lib/logger.cjs +30 -0
- package/mcp-server/logger.js +173 -0
- package/mcp-server/package.json +2 -2
- package/mcp-server/premium-tools.js +2 -2
- package/mcp-server/tier-auth.js +245 -35
- package/mcp-server/truth-firewall-tools.js +145 -15
- package/mcp-server/vibecheck-tools.js +2 -2
- package/package.json +2 -3
- package/mcp-server/index.old.js +0 -4137
- package/mcp-server/package-lock.json +0 -165
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Policy Loader
|
|
3
3
|
*
|
|
4
|
-
* Loads and validates agent firewall policy from
|
|
4
|
+
* Loads and validates agent firewall policy from:
|
|
5
|
+
* - .vibecheck/policy.json (JSON policy)
|
|
6
|
+
* - .vibecheck/rules.yaml (YAML rule DSL)
|
|
7
|
+
* - .vibecheckrc (legacy)
|
|
8
|
+
*
|
|
5
9
|
* Falls back to default policy if not found.
|
|
6
10
|
*/
|
|
7
11
|
|
|
@@ -11,6 +15,14 @@ const fs = require("fs");
|
|
|
11
15
|
const path = require("path");
|
|
12
16
|
const Ajv = require("ajv").default || require("ajv");
|
|
13
17
|
|
|
18
|
+
// Try to load YAML parser
|
|
19
|
+
let yaml = null;
|
|
20
|
+
try {
|
|
21
|
+
yaml = require("js-yaml");
|
|
22
|
+
} catch {
|
|
23
|
+
// js-yaml not available, YAML support disabled
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
// Load schema
|
|
15
27
|
const schemaPath = path.join(__dirname, "schema.json");
|
|
16
28
|
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
|
|
@@ -19,6 +31,94 @@ const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
|
|
|
19
31
|
const defaultPolicyPath = path.join(__dirname, "default-policy.json");
|
|
20
32
|
const defaultPolicy = JSON.parse(fs.readFileSync(defaultPolicyPath, "utf8"));
|
|
21
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Load YAML rules file
|
|
36
|
+
* @param {string} rulesPath - Path to rules.yaml
|
|
37
|
+
* @returns {Array} Parsed rules array
|
|
38
|
+
*/
|
|
39
|
+
function loadYamlRules(rulesPath) {
|
|
40
|
+
if (!yaml) {
|
|
41
|
+
console.warn("js-yaml not installed, YAML rules not loaded");
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const content = fs.readFileSync(rulesPath, "utf8");
|
|
47
|
+
const parsed = yaml.load(content);
|
|
48
|
+
|
|
49
|
+
if (!parsed || !parsed.rules) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return normalizeYamlRules(parsed.rules);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.warn(`Failed to load YAML rules: ${error.message}`);
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Normalize YAML rules to policy format
|
|
62
|
+
* @param {Array} rules - Raw YAML rules
|
|
63
|
+
* @returns {Array} Normalized rules
|
|
64
|
+
*/
|
|
65
|
+
function normalizeYamlRules(rules) {
|
|
66
|
+
return rules.map(rule => {
|
|
67
|
+
// Map YAML DSL actions to policy severity
|
|
68
|
+
const severityMap = {
|
|
69
|
+
block: "block",
|
|
70
|
+
warn: "warn",
|
|
71
|
+
allow: "allow",
|
|
72
|
+
require_confirmation: "warn",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
id: rule.id,
|
|
77
|
+
description: rule.description || "",
|
|
78
|
+
severity: severityMap[rule.action] || "warn",
|
|
79
|
+
enabled: rule.enabled !== false,
|
|
80
|
+
match: rule.match || {},
|
|
81
|
+
action: rule.action || "warn",
|
|
82
|
+
message: rule.message || rule.description || "",
|
|
83
|
+
// Custom rule evaluator info
|
|
84
|
+
custom: rule.custom || null,
|
|
85
|
+
block_if_domain: rule.block_if_domain || [],
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Merge YAML rules into policy
|
|
92
|
+
* @param {object} policy - Policy object
|
|
93
|
+
* @param {Array} yamlRules - Normalized YAML rules
|
|
94
|
+
* @returns {object} Policy with merged rules
|
|
95
|
+
*/
|
|
96
|
+
function mergeYamlRulesIntoPolicy(policy, yamlRules) {
|
|
97
|
+
if (!yamlRules || yamlRules.length === 0) {
|
|
98
|
+
return policy;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const merged = { ...policy };
|
|
102
|
+
merged.rules = merged.rules || {};
|
|
103
|
+
merged.customRules = merged.customRules || [];
|
|
104
|
+
|
|
105
|
+
for (const rule of yamlRules) {
|
|
106
|
+
// Add to custom rules array
|
|
107
|
+
merged.customRules.push(rule);
|
|
108
|
+
|
|
109
|
+
// Also add to rules object for backward compatibility
|
|
110
|
+
if (rule.id) {
|
|
111
|
+
merged.rules[rule.id] = {
|
|
112
|
+
severity: rule.severity,
|
|
113
|
+
enabled: rule.enabled,
|
|
114
|
+
block_if_domain: rule.block_if_domain,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return merged;
|
|
120
|
+
}
|
|
121
|
+
|
|
22
122
|
/**
|
|
23
123
|
* Load policy from project root
|
|
24
124
|
* @param {string} projectRoot - Project root directory
|
|
@@ -27,28 +127,52 @@ const defaultPolicy = JSON.parse(fs.readFileSync(defaultPolicyPath, "utf8"));
|
|
|
27
127
|
*/
|
|
28
128
|
function loadPolicy(projectRoot, overrides = {}) {
|
|
29
129
|
const policyPath = path.join(projectRoot, ".vibecheck", "policy.json");
|
|
130
|
+
const yamlRulesPath = path.join(projectRoot, ".vibecheck", "rules.yaml");
|
|
131
|
+
const legacyRcPath = path.join(projectRoot, ".vibecheckrc");
|
|
30
132
|
|
|
31
133
|
let policy;
|
|
32
134
|
|
|
33
|
-
// Try to load project policy
|
|
135
|
+
// Try to load project policy (JSON)
|
|
34
136
|
if (fs.existsSync(policyPath)) {
|
|
35
137
|
try {
|
|
36
138
|
policy = JSON.parse(fs.readFileSync(policyPath, "utf8"));
|
|
37
139
|
} catch (error) {
|
|
38
140
|
throw new Error(`Failed to parse policy file: ${error.message}`);
|
|
39
141
|
}
|
|
142
|
+
} else if (fs.existsSync(legacyRcPath)) {
|
|
143
|
+
// Try legacy .vibecheckrc
|
|
144
|
+
try {
|
|
145
|
+
policy = JSON.parse(fs.readFileSync(legacyRcPath, "utf8"));
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// Might be YAML format
|
|
148
|
+
if (yaml) {
|
|
149
|
+
try {
|
|
150
|
+
policy = yaml.load(fs.readFileSync(legacyRcPath, "utf8"));
|
|
151
|
+
} catch {
|
|
152
|
+
throw new Error(`Failed to parse .vibecheckrc: ${error.message}`);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
throw new Error(`Failed to parse .vibecheckrc: ${error.message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
40
158
|
} else {
|
|
41
159
|
// Use default policy
|
|
42
160
|
policy = JSON.parse(JSON.stringify(defaultPolicy));
|
|
43
161
|
}
|
|
44
162
|
|
|
163
|
+
// Load and merge YAML rules if present
|
|
164
|
+
if (fs.existsSync(yamlRulesPath)) {
|
|
165
|
+
const yamlRules = loadYamlRules(yamlRulesPath);
|
|
166
|
+
policy = mergeYamlRulesIntoPolicy(policy, yamlRules);
|
|
167
|
+
}
|
|
168
|
+
|
|
45
169
|
// Apply overrides (deep merge)
|
|
46
170
|
if (Object.keys(overrides).length > 0) {
|
|
47
171
|
policy = deepMerge(policy, overrides);
|
|
48
172
|
}
|
|
49
173
|
|
|
50
174
|
// Validate against schema
|
|
51
|
-
const ajv = new Ajv({ allErrors: true });
|
|
175
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
52
176
|
const validate = ajv.compile(schema);
|
|
53
177
|
const valid = validate(policy);
|
|
54
178
|
|
|
@@ -120,7 +244,7 @@ function savePolicy(projectRoot, policy) {
|
|
|
120
244
|
}
|
|
121
245
|
|
|
122
246
|
// Validate before saving
|
|
123
|
-
const ajv = new Ajv({ allErrors: true });
|
|
247
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
124
248
|
const validate = ajv.compile(schema);
|
|
125
249
|
const valid = validate(policy);
|
|
126
250
|
|
|
@@ -134,10 +258,194 @@ function savePolicy(projectRoot, policy) {
|
|
|
134
258
|
fs.writeFileSync(policyPath, JSON.stringify(policy, null, 2));
|
|
135
259
|
}
|
|
136
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Save YAML rules to project root
|
|
263
|
+
* @param {string} projectRoot - Project root directory
|
|
264
|
+
* @param {Array} rules - Rules array
|
|
265
|
+
*/
|
|
266
|
+
function saveYamlRules(projectRoot, rules) {
|
|
267
|
+
if (!yaml) {
|
|
268
|
+
throw new Error("js-yaml not installed, cannot save YAML rules");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const vibecheckDir = path.join(projectRoot, ".vibecheck");
|
|
272
|
+
const rulesPath = path.join(vibecheckDir, "rules.yaml");
|
|
273
|
+
|
|
274
|
+
// Ensure .vibecheck directory exists
|
|
275
|
+
if (!fs.existsSync(vibecheckDir)) {
|
|
276
|
+
fs.mkdirSync(vibecheckDir, { recursive: true });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const content = yaml.dump({ rules }, {
|
|
280
|
+
lineWidth: 120,
|
|
281
|
+
quotingType: '"',
|
|
282
|
+
forceQuotes: false,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
fs.writeFileSync(rulesPath, content);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Create default YAML rules file
|
|
290
|
+
* @param {string} projectRoot - Project root directory
|
|
291
|
+
*/
|
|
292
|
+
function createDefaultYamlRules(projectRoot) {
|
|
293
|
+
const defaultRules = [
|
|
294
|
+
{
|
|
295
|
+
id: "no-phantom-env",
|
|
296
|
+
description: "Block undeclared environment variables",
|
|
297
|
+
match: { type: "env", exists: false },
|
|
298
|
+
action: "block",
|
|
299
|
+
message: "Env vars must be declared before use",
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: "no-ghost-routes",
|
|
303
|
+
description: "Block routes that reference unregistered paths",
|
|
304
|
+
match: { type: "route", registered: false },
|
|
305
|
+
action: "block",
|
|
306
|
+
message: "Routes must be registered in the router",
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: "core-folder-protection",
|
|
310
|
+
description: "Require confirmation for changes to core code",
|
|
311
|
+
match: { path: "^src/core/" },
|
|
312
|
+
action: "require_confirmation",
|
|
313
|
+
message: "Changes to core require explicit confirmation",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: "no-silent-deletes",
|
|
317
|
+
description: "Require confirmation for file deletions",
|
|
318
|
+
match: { operation: "delete" },
|
|
319
|
+
action: "require_confirmation",
|
|
320
|
+
message: "File deletions require explicit confirmation",
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: "auth-changes-high-risk",
|
|
324
|
+
description: "Flag authentication changes as high risk",
|
|
325
|
+
match: { tags: ["auth", "security"] },
|
|
326
|
+
action: "warn",
|
|
327
|
+
block_if_domain: ["payments"],
|
|
328
|
+
message: "Auth changes require careful review",
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
id: "max-files-per-change",
|
|
332
|
+
description: "Limit number of files in a single change",
|
|
333
|
+
match: { files_count: { gt: 10 } },
|
|
334
|
+
action: "require_confirmation",
|
|
335
|
+
message: "Large changes (>10 files) require confirmation",
|
|
336
|
+
},
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
saveYamlRules(projectRoot, defaultRules);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get custom rules from policy
|
|
344
|
+
* @param {object} policy - Policy object
|
|
345
|
+
* @returns {Array} Custom rules
|
|
346
|
+
*/
|
|
347
|
+
function getCustomRules(policy) {
|
|
348
|
+
return policy.customRules || [];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Evaluate a custom YAML rule against a change
|
|
353
|
+
* @param {object} rule - Rule definition
|
|
354
|
+
* @param {object} context - Change context
|
|
355
|
+
* @returns {object|null} Violation if rule triggered, null otherwise
|
|
356
|
+
*/
|
|
357
|
+
function evaluateCustomRule(rule, context) {
|
|
358
|
+
if (!rule.enabled) return null;
|
|
359
|
+
|
|
360
|
+
const match = rule.match || {};
|
|
361
|
+
let triggered = false;
|
|
362
|
+
|
|
363
|
+
// Match by type
|
|
364
|
+
if (match.type) {
|
|
365
|
+
const claims = context.claims || [];
|
|
366
|
+
const typeClaims = claims.filter(c => c.type === match.type);
|
|
367
|
+
|
|
368
|
+
if (typeClaims.length > 0) {
|
|
369
|
+
if (match.exists === false) {
|
|
370
|
+
// Check if any claim doesn't exist
|
|
371
|
+
triggered = typeClaims.some(c => !c.exists);
|
|
372
|
+
} else if (match.registered === false) {
|
|
373
|
+
triggered = typeClaims.some(c => !c.registered);
|
|
374
|
+
} else {
|
|
375
|
+
triggered = true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Match by path pattern
|
|
381
|
+
if (match.path) {
|
|
382
|
+
const files = context.files || [];
|
|
383
|
+
const regex = new RegExp(match.path);
|
|
384
|
+
triggered = triggered || files.some(f => regex.test(f.path || f));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Match by operation
|
|
388
|
+
if (match.operation) {
|
|
389
|
+
const operations = context.operations || [];
|
|
390
|
+
triggered = triggered || operations.some(op => op.type === match.operation);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Match by file count
|
|
394
|
+
if (match.files_count) {
|
|
395
|
+
const fileCount = (context.files || []).length;
|
|
396
|
+
if (match.files_count.gt && fileCount > match.files_count.gt) {
|
|
397
|
+
triggered = true;
|
|
398
|
+
}
|
|
399
|
+
if (match.files_count.lt && fileCount < match.files_count.lt) {
|
|
400
|
+
triggered = true;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Match by tags
|
|
405
|
+
if (match.tags && Array.isArray(match.tags)) {
|
|
406
|
+
const domains = context.domains || [];
|
|
407
|
+
triggered = triggered || match.tags.some(tag => domains.includes(tag));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!triggered) return null;
|
|
411
|
+
|
|
412
|
+
// Check block_if_domain upgrade
|
|
413
|
+
let severity = rule.severity || rule.action;
|
|
414
|
+
if (rule.block_if_domain && rule.block_if_domain.length > 0) {
|
|
415
|
+
const domains = context.domains || [];
|
|
416
|
+
if (rule.block_if_domain.some(d => domains.includes(d))) {
|
|
417
|
+
severity = "block";
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
rule: rule.id,
|
|
423
|
+
type: "custom_rule",
|
|
424
|
+
severity,
|
|
425
|
+
message: rule.message || rule.description || `Rule ${rule.id} triggered`,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Check if YAML support is available
|
|
431
|
+
* @returns {boolean} YAML support available
|
|
432
|
+
*/
|
|
433
|
+
function hasYamlSupport() {
|
|
434
|
+
return yaml !== null;
|
|
435
|
+
}
|
|
436
|
+
|
|
137
437
|
module.exports = {
|
|
138
438
|
loadPolicy,
|
|
139
439
|
getDefaultPolicy,
|
|
140
440
|
savePolicy,
|
|
441
|
+
loadYamlRules,
|
|
442
|
+
saveYamlRules,
|
|
443
|
+
createDefaultYamlRules,
|
|
444
|
+
getCustomRules,
|
|
445
|
+
evaluateCustomRule,
|
|
446
|
+
mergeYamlRulesIntoPolicy,
|
|
447
|
+
normalizeYamlRules,
|
|
448
|
+
hasYamlSupport,
|
|
141
449
|
schema,
|
|
142
450
|
defaultPolicy
|
|
143
451
|
};
|
|
@@ -2,12 +2,97 @@
|
|
|
2
2
|
* Ghost Env Rule
|
|
3
3
|
*
|
|
4
4
|
* Blocks if process.env.X used but not declared.
|
|
5
|
+
* Includes smart whitelisting to reduce false positives.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
"use strict";
|
|
8
9
|
|
|
9
10
|
const { CLAIM_TYPES } = require("../../claims/claim-types");
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Common environment variables that are safe to use without explicit declaration.
|
|
14
|
+
* These are well-known Node.js, Next.js, and common deployment platform vars.
|
|
15
|
+
*/
|
|
16
|
+
const SAFE_ENV_VARS = new Set([
|
|
17
|
+
// Node.js core
|
|
18
|
+
"NODE_ENV",
|
|
19
|
+
"NODE_OPTIONS",
|
|
20
|
+
"NODE_PATH",
|
|
21
|
+
"NODE_DEBUG",
|
|
22
|
+
|
|
23
|
+
// Next.js / React
|
|
24
|
+
"NEXT_PUBLIC_", // Prefix pattern
|
|
25
|
+
"REACT_APP_", // Prefix pattern
|
|
26
|
+
"VERCEL",
|
|
27
|
+
"VERCEL_ENV",
|
|
28
|
+
"VERCEL_URL",
|
|
29
|
+
"NEXT_RUNTIME",
|
|
30
|
+
|
|
31
|
+
// Common deployment
|
|
32
|
+
"PORT",
|
|
33
|
+
"HOST",
|
|
34
|
+
"HOSTNAME",
|
|
35
|
+
"CI",
|
|
36
|
+
"DEBUG",
|
|
37
|
+
"LOG_LEVEL",
|
|
38
|
+
"TZ",
|
|
39
|
+
"LANG",
|
|
40
|
+
"HOME",
|
|
41
|
+
"USER",
|
|
42
|
+
"PATH",
|
|
43
|
+
"PWD",
|
|
44
|
+
"SHELL",
|
|
45
|
+
"TERM",
|
|
46
|
+
|
|
47
|
+
// Testing
|
|
48
|
+
"TEST",
|
|
49
|
+
"JEST_WORKER_ID",
|
|
50
|
+
"VITEST",
|
|
51
|
+
|
|
52
|
+
// Build tools
|
|
53
|
+
"npm_package_name",
|
|
54
|
+
"npm_package_version",
|
|
55
|
+
"npm_lifecycle_event",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Prefix patterns that are safe
|
|
60
|
+
*/
|
|
61
|
+
const SAFE_ENV_PREFIXES = [
|
|
62
|
+
"NEXT_PUBLIC_",
|
|
63
|
+
"REACT_APP_",
|
|
64
|
+
"VITE_",
|
|
65
|
+
"npm_",
|
|
66
|
+
"GITHUB_",
|
|
67
|
+
"CI_",
|
|
68
|
+
"VERCEL_",
|
|
69
|
+
"RAILWAY_",
|
|
70
|
+
"RENDER_",
|
|
71
|
+
"HEROKU_",
|
|
72
|
+
"AWS_",
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if an env var is in the safe list
|
|
77
|
+
* @param {string} varName - Environment variable name
|
|
78
|
+
* @returns {boolean} Is safe
|
|
79
|
+
*/
|
|
80
|
+
function isSafeEnvVar(varName) {
|
|
81
|
+
if (!varName || typeof varName !== 'string') return false;
|
|
82
|
+
|
|
83
|
+
const normalized = varName.toUpperCase();
|
|
84
|
+
|
|
85
|
+
// Direct match
|
|
86
|
+
if (SAFE_ENV_VARS.has(normalized)) return true;
|
|
87
|
+
|
|
88
|
+
// Prefix match
|
|
89
|
+
for (const prefix of SAFE_ENV_PREFIXES) {
|
|
90
|
+
if (normalized.startsWith(prefix)) return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
11
96
|
/**
|
|
12
97
|
* Evaluate ghost env rule
|
|
13
98
|
* @param {object} params
|
|
@@ -23,6 +108,9 @@ function evaluate({ claims, evidence, policy }) {
|
|
|
23
108
|
return null;
|
|
24
109
|
}
|
|
25
110
|
|
|
111
|
+
// Get custom whitelist from policy
|
|
112
|
+
const customWhitelist = new Set(ruleConfig.whitelist || []);
|
|
113
|
+
|
|
26
114
|
// Find env claims with UNPROVEN evidence
|
|
27
115
|
for (let i = 0; i < claims.length; i++) {
|
|
28
116
|
const claim = claims[i];
|
|
@@ -31,10 +119,34 @@ function evaluate({ claims, evidence, policy }) {
|
|
|
31
119
|
const ev = evidence.find(e => e.claimId === `claim_${i}`);
|
|
32
120
|
|
|
33
121
|
if (ev && ev.result === "UNPROVEN") {
|
|
122
|
+
const envVar = String(claim.value || claim.key || "");
|
|
123
|
+
|
|
124
|
+
// Skip if it's a known safe env var
|
|
125
|
+
if (isSafeEnvVar(envVar)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Skip if in custom whitelist
|
|
130
|
+
if (customWhitelist.has(envVar)) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Skip if it's a fallback pattern (e.g., process.env.X || 'default')
|
|
135
|
+
if (claim.hasFallback) {
|
|
136
|
+
// Downgrade to warning instead of block
|
|
137
|
+
return {
|
|
138
|
+
rule: "ghost_env",
|
|
139
|
+
severity: "warn",
|
|
140
|
+
message: `Env var ${envVar} is used with fallback but not declared`,
|
|
141
|
+
claimId: `claim_${i}`,
|
|
142
|
+
claim
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
34
146
|
return {
|
|
35
147
|
rule: "ghost_env",
|
|
36
148
|
severity: ruleConfig.severity || "block",
|
|
37
|
-
message: `Ghost env var: ${
|
|
149
|
+
message: `Ghost env var: ${envVar} is used but not declared`,
|
|
38
150
|
claimId: `claim_${i}`,
|
|
39
151
|
claim
|
|
40
152
|
};
|
|
@@ -2,12 +2,120 @@
|
|
|
2
2
|
* Ghost Route Rule
|
|
3
3
|
*
|
|
4
4
|
* Blocks if UI references route not registered in truthpack.
|
|
5
|
+
* Includes smart detection of external APIs and common patterns.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
"use strict";
|
|
8
9
|
|
|
9
10
|
const { CLAIM_TYPES } = require("../../claims/claim-types");
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Patterns that indicate an external API (not a local route)
|
|
14
|
+
*/
|
|
15
|
+
const EXTERNAL_API_PATTERNS = [
|
|
16
|
+
// Full URLs
|
|
17
|
+
/^https?:\/\//i,
|
|
18
|
+
/^\/\/[a-z]/i,
|
|
19
|
+
|
|
20
|
+
// Common external API domains
|
|
21
|
+
/api\.github\.com/i,
|
|
22
|
+
/api\.stripe\.com/i,
|
|
23
|
+
/api\.openai\.com/i,
|
|
24
|
+
/api\.anthropic\.com/i,
|
|
25
|
+
/api\.twilio\.com/i,
|
|
26
|
+
/api\.sendgrid\.com/i,
|
|
27
|
+
/graph\.facebook\.com/i,
|
|
28
|
+
/api\.twitter\.com/i,
|
|
29
|
+
/googleapis\.com/i,
|
|
30
|
+
/aws\.amazon\.com/i,
|
|
31
|
+
/cloudflare\.com/i,
|
|
32
|
+
/api\.clerk\.dev/i,
|
|
33
|
+
/api\.auth0\.com/i,
|
|
34
|
+
/api\.supabase\.co/i,
|
|
35
|
+
/api\.vercel\.app/i,
|
|
36
|
+
|
|
37
|
+
// GraphQL endpoints
|
|
38
|
+
/graphql/i,
|
|
39
|
+
|
|
40
|
+
// Webhook patterns
|
|
41
|
+
/webhook/i,
|
|
42
|
+
/hook\//i,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Path patterns that are safe to skip (dynamic or template)
|
|
47
|
+
*/
|
|
48
|
+
const SAFE_ROUTE_PATTERNS = [
|
|
49
|
+
// Dynamic segments
|
|
50
|
+
/\$\{/, // Template literals
|
|
51
|
+
/\[.*\]/, // Dynamic route segments [id]
|
|
52
|
+
/:\w+/, // URL params :id
|
|
53
|
+
/\{\{.*\}\}/, // Template syntax
|
|
54
|
+
|
|
55
|
+
// Hash-only routes
|
|
56
|
+
/^#/,
|
|
57
|
+
|
|
58
|
+
// Query-only
|
|
59
|
+
/^\?/,
|
|
60
|
+
|
|
61
|
+
// Empty or undefined
|
|
62
|
+
/^undefined$/i,
|
|
63
|
+
/^null$/i,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a route is external
|
|
68
|
+
* @param {string} route - Route path
|
|
69
|
+
* @returns {boolean} Is external
|
|
70
|
+
*/
|
|
71
|
+
function isExternalRoute(route) {
|
|
72
|
+
if (!route || typeof route !== 'string') return true;
|
|
73
|
+
|
|
74
|
+
const trimmed = route.trim();
|
|
75
|
+
|
|
76
|
+
// Check external patterns
|
|
77
|
+
for (const pattern of EXTERNAL_API_PATTERNS) {
|
|
78
|
+
if (pattern.test(trimmed)) return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if a route should be skipped
|
|
86
|
+
* @param {string} route - Route path
|
|
87
|
+
* @returns {boolean} Should skip
|
|
88
|
+
*/
|
|
89
|
+
function shouldSkipRoute(route) {
|
|
90
|
+
if (!route || typeof route !== 'string') return true;
|
|
91
|
+
|
|
92
|
+
const trimmed = route.trim();
|
|
93
|
+
|
|
94
|
+
// Skip empty routes
|
|
95
|
+
if (trimmed.length === 0) return true;
|
|
96
|
+
|
|
97
|
+
// Check safe patterns
|
|
98
|
+
for (const pattern of SAFE_ROUTE_PATTERNS) {
|
|
99
|
+
if (pattern.test(trimmed)) return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if route is a local API route
|
|
107
|
+
* @param {string} route - Route path
|
|
108
|
+
* @returns {boolean} Is local API
|
|
109
|
+
*/
|
|
110
|
+
function isLocalApiRoute(route) {
|
|
111
|
+
if (!route || typeof route !== 'string') return false;
|
|
112
|
+
|
|
113
|
+
const trimmed = route.trim().toLowerCase();
|
|
114
|
+
|
|
115
|
+
// Must start with /api/ to be a local Next.js API route
|
|
116
|
+
return trimmed.startsWith('/api/');
|
|
117
|
+
}
|
|
118
|
+
|
|
11
119
|
/**
|
|
12
120
|
* Evaluate ghost route rule
|
|
13
121
|
* @param {object} params
|
|
@@ -23,6 +131,9 @@ function evaluate({ claims, evidence, policy }) {
|
|
|
23
131
|
return null;
|
|
24
132
|
}
|
|
25
133
|
|
|
134
|
+
// Get custom whitelist from policy
|
|
135
|
+
const customWhitelist = new Set(ruleConfig.whitelist || []);
|
|
136
|
+
|
|
26
137
|
// Find route claims with UNPROVEN evidence
|
|
27
138
|
for (let i = 0; i < claims.length; i++) {
|
|
28
139
|
const claim = claims[i];
|
|
@@ -30,23 +141,39 @@ function evaluate({ claims, evidence, policy }) {
|
|
|
30
141
|
if (claim.type === CLAIM_TYPES.ROUTE || claim.type === CLAIM_TYPES.HTTP_CALL) {
|
|
31
142
|
const ev = evidence.find(e => e.claimId === `claim_${i}`);
|
|
32
143
|
|
|
33
|
-
// Skip if evidence shows it's
|
|
34
|
-
if (ev &&
|
|
144
|
+
// Skip if evidence shows it's already proven
|
|
145
|
+
if (ev && ev.result === "PROVEN") {
|
|
35
146
|
continue;
|
|
36
147
|
}
|
|
37
148
|
|
|
38
149
|
if (ev && ev.result === "UNPROVEN") {
|
|
39
|
-
// Double-check: skip external API calls (routes not starting with /api/)
|
|
40
150
|
const routePath = String(claim.value || "").trim();
|
|
41
|
-
|
|
42
|
-
|
|
151
|
+
|
|
152
|
+
// Skip dynamic or template routes
|
|
153
|
+
if (shouldSkipRoute(routePath)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Skip external API calls
|
|
158
|
+
if (isExternalRoute(routePath)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Only flag local API routes
|
|
163
|
+
if (!isLocalApiRoute(routePath)) {
|
|
164
|
+
// Not a local API route - could be page navigation, skip
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Skip if in custom whitelist
|
|
169
|
+
if (customWhitelist.has(routePath)) {
|
|
43
170
|
continue;
|
|
44
171
|
}
|
|
45
172
|
|
|
46
173
|
return {
|
|
47
174
|
rule: "ghost_route",
|
|
48
175
|
severity: ruleConfig.severity || "block",
|
|
49
|
-
message: `Ghost route: ${
|
|
176
|
+
message: `Ghost route: ${routePath} is referenced but not registered in truthpack`,
|
|
50
177
|
claimId: `claim_${i}`,
|
|
51
178
|
claim
|
|
52
179
|
};
|