@node9/proxy 1.67.1 → 1.67.3
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/dist/cli.js +1776 -1334
- package/dist/cli.mjs +1743 -1301
- package/dist/dashboard.mjs +246 -133
- package/dist/index.js +676 -239
- package/dist/index.mjs +676 -239
- package/dist/scan-ink.mjs +42 -0
- package/package.json +1 -1
package/dist/dashboard.mjs
CHANGED
|
@@ -184,6 +184,125 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
184
184
|
}
|
|
185
185
|
return null;
|
|
186
186
|
}
|
|
187
|
+
function validateRegex(pattern) {
|
|
188
|
+
if (!pattern) return "Pattern is required";
|
|
189
|
+
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
190
|
+
try {
|
|
191
|
+
new RegExp(pattern);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return `Invalid regex syntax: ${e.message}`;
|
|
194
|
+
}
|
|
195
|
+
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
196
|
+
if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
function getCompiledRegex(pattern, flags = "") {
|
|
200
|
+
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
201
|
+
const key = `${pattern}\0${flags}`;
|
|
202
|
+
if (regexCache.has(key)) {
|
|
203
|
+
const cached = regexCache.get(key);
|
|
204
|
+
regexCache.delete(key);
|
|
205
|
+
regexCache.set(key, cached);
|
|
206
|
+
return cached;
|
|
207
|
+
}
|
|
208
|
+
if (validateRegex(pattern) !== null) return null;
|
|
209
|
+
try {
|
|
210
|
+
const re = new RegExp(pattern, flags);
|
|
211
|
+
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
212
|
+
const oldest = regexCache.keys().next().value;
|
|
213
|
+
if (oldest) regexCache.delete(oldest);
|
|
214
|
+
}
|
|
215
|
+
regexCache.set(key, re);
|
|
216
|
+
return re;
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function matchesPattern(text, patterns) {
|
|
222
|
+
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
223
|
+
if (p.length === 0) return false;
|
|
224
|
+
const isMatch = pm(p, { nocase: true, dot: true });
|
|
225
|
+
const target = text.toLowerCase();
|
|
226
|
+
const directMatch = isMatch(target);
|
|
227
|
+
if (directMatch) return true;
|
|
228
|
+
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
229
|
+
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
230
|
+
}
|
|
231
|
+
function getNestedValue(obj, path11) {
|
|
232
|
+
if (!obj || typeof obj !== "object") return null;
|
|
233
|
+
const segments = path11.split(".");
|
|
234
|
+
for (const seg of segments) {
|
|
235
|
+
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
236
|
+
}
|
|
237
|
+
return segments.reduce((prev, curr) => prev?.[curr], obj);
|
|
238
|
+
}
|
|
239
|
+
function evaluateSmartConditions(args, rule) {
|
|
240
|
+
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
241
|
+
const mode = rule.conditionMode ?? "all";
|
|
242
|
+
const fieldCache = /* @__PURE__ */ new Map();
|
|
243
|
+
const resolveField = (field) => {
|
|
244
|
+
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
245
|
+
const rawVal = getNestedValue(args, field);
|
|
246
|
+
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
247
|
+
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
248
|
+
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
249
|
+
fieldCache.set(field, val);
|
|
250
|
+
return val;
|
|
251
|
+
};
|
|
252
|
+
const readingsCache = /* @__PURE__ */ new Map();
|
|
253
|
+
const resolveFieldReadings = (field) => {
|
|
254
|
+
const cached = readingsCache.get(field);
|
|
255
|
+
if (cached) return cached;
|
|
256
|
+
const primary = resolveField(field);
|
|
257
|
+
if (primary === null) {
|
|
258
|
+
readingsCache.set(field, []);
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
let out = [primary];
|
|
262
|
+
if (field === "command") {
|
|
263
|
+
const raw = getNestedValue(args, field);
|
|
264
|
+
if (typeof raw === "string") {
|
|
265
|
+
const collapsed = commandReadings(raw).map((r) => r.replace(/\s+/g, " ").trim());
|
|
266
|
+
out = [.../* @__PURE__ */ new Set([primary, ...collapsed])];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
readingsCache.set(field, out);
|
|
270
|
+
return out;
|
|
271
|
+
};
|
|
272
|
+
const results = rule.conditions.map((cond) => {
|
|
273
|
+
const val = resolveField(cond.field);
|
|
274
|
+
switch (cond.op) {
|
|
275
|
+
case "exists":
|
|
276
|
+
return val !== null && val !== "";
|
|
277
|
+
case "notExists":
|
|
278
|
+
return val === null || val === "";
|
|
279
|
+
case "contains":
|
|
280
|
+
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
281
|
+
case "notContains":
|
|
282
|
+
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
283
|
+
case "matches": {
|
|
284
|
+
if (val === null || !cond.value) return false;
|
|
285
|
+
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
286
|
+
if (!reM) return false;
|
|
287
|
+
return resolveFieldReadings(cond.field).some((v) => reM.test(v));
|
|
288
|
+
}
|
|
289
|
+
case "notMatches": {
|
|
290
|
+
if (!cond.value) return false;
|
|
291
|
+
if (val === null) return true;
|
|
292
|
+
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
293
|
+
if (!reN) return false;
|
|
294
|
+
return !resolveFieldReadings(cond.field).some((v) => reN.test(v));
|
|
295
|
+
}
|
|
296
|
+
case "matchesGlob":
|
|
297
|
+
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
298
|
+
case "notMatchesGlob":
|
|
299
|
+
return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
|
|
300
|
+
default:
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
305
|
+
}
|
|
187
306
|
function isCatHeredocOrLit(part) {
|
|
188
307
|
if (!part) return false;
|
|
189
308
|
const t = syntax.NodeType(part);
|
|
@@ -237,14 +356,22 @@ function cachedNormalize(command, compute) {
|
|
|
237
356
|
return result;
|
|
238
357
|
}
|
|
239
358
|
function normalizeCommandForPolicy(command) {
|
|
359
|
+
return commandReadingsImpl(command).posix;
|
|
360
|
+
}
|
|
361
|
+
function commandReadings(command) {
|
|
362
|
+
const r = commandReadingsImpl(command);
|
|
363
|
+
return r.separator === r.posix ? [r.posix] : [r.posix, r.separator];
|
|
364
|
+
}
|
|
365
|
+
function commandReadingsImpl(command) {
|
|
240
366
|
return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
|
|
241
367
|
}
|
|
242
368
|
function normalizeCommandForPolicyImpl(command) {
|
|
243
369
|
const f = parseShared(command);
|
|
244
|
-
if (f === PARSE_FAIL) return command;
|
|
370
|
+
if (f === PARSE_FAIL) return { posix: command, separator: command };
|
|
245
371
|
try {
|
|
246
372
|
const strips = [];
|
|
247
373
|
const rewrites = [];
|
|
374
|
+
const quoteOnlyRewrites = [];
|
|
248
375
|
const msgSpans = /* @__PURE__ */ new Set();
|
|
249
376
|
syntax.Walk(f, (node) => {
|
|
250
377
|
if (!node) return false;
|
|
@@ -289,22 +416,23 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
289
416
|
if (resolved === source) continue;
|
|
290
417
|
if (resolved === "" || /\s/.test(resolved)) continue;
|
|
291
418
|
rewrites.push([s, e, resolved]);
|
|
419
|
+
const quoteOnly = source.replace(/['"]/g, "");
|
|
420
|
+
if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
|
|
292
421
|
}
|
|
293
422
|
return true;
|
|
294
423
|
});
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
...
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
return result;
|
|
424
|
+
const stripEdits = strips.map(([s, e]) => [s, e, '""']);
|
|
425
|
+
const apply = (extra) => {
|
|
426
|
+
const edits = [...stripEdits, ...extra];
|
|
427
|
+
if (edits.length === 0) return command;
|
|
428
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
429
|
+
let out = command;
|
|
430
|
+
for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
|
|
431
|
+
return out;
|
|
432
|
+
};
|
|
433
|
+
return { posix: apply(rewrites), separator: apply(quoteOnlyRewrites) };
|
|
306
434
|
} catch {
|
|
307
|
-
return command;
|
|
435
|
+
return { posix: command, separator: command };
|
|
308
436
|
}
|
|
309
437
|
}
|
|
310
438
|
function scanArgsForDynamicExec(args, startIdx) {
|
|
@@ -359,6 +487,20 @@ function detectDangerousShellExec(command) {
|
|
|
359
487
|
return null;
|
|
360
488
|
}
|
|
361
489
|
}
|
|
490
|
+
function isBashTool(toolName) {
|
|
491
|
+
return BASH_TOOL_NAMES.has(toolName.toLowerCase());
|
|
492
|
+
}
|
|
493
|
+
function isShellShapedTool(toolName, toolInspection) {
|
|
494
|
+
if (isBashTool(toolName)) return true;
|
|
495
|
+
if (!toolInspection) return false;
|
|
496
|
+
const pattern = Object.keys(toolInspection).find((p) => matchesPattern(toolName, p));
|
|
497
|
+
return pattern !== void 0 && toolInspection[pattern] === "command";
|
|
498
|
+
}
|
|
499
|
+
function toolMatchesRule(toolName, ruleTool, toolInspection) {
|
|
500
|
+
if (!ruleTool) return true;
|
|
501
|
+
if (matchesPattern(toolName, ruleTool)) return true;
|
|
502
|
+
return isShellShapedTool(toolName, toolInspection) && matchesPattern("bash", ruleTool);
|
|
503
|
+
}
|
|
362
504
|
function isProtectedHomePath(rawPath) {
|
|
363
505
|
let p = rawPath.replace(/^\$HOME[\\/]?|^\$\{HOME\}[\\/]?/, "~/");
|
|
364
506
|
let underHome = false;
|
|
@@ -517,105 +659,6 @@ function analyzeFsOperationImpl(command) {
|
|
|
517
659
|
return null;
|
|
518
660
|
}
|
|
519
661
|
}
|
|
520
|
-
function validateRegex(pattern) {
|
|
521
|
-
if (!pattern) return "Pattern is required";
|
|
522
|
-
if (pattern.length > MAX_REGEX_LENGTH) return `Pattern exceeds max length of ${MAX_REGEX_LENGTH}`;
|
|
523
|
-
try {
|
|
524
|
-
new RegExp(pattern);
|
|
525
|
-
} catch (e) {
|
|
526
|
-
return `Invalid regex syntax: ${e.message}`;
|
|
527
|
-
}
|
|
528
|
-
if (/\\\d+[*+{]/.test(pattern)) return "Quantified backreferences are forbidden (ReDoS risk)";
|
|
529
|
-
if (!safeRegex2(pattern)) return "Pattern rejected: potential ReDoS vulnerability detected";
|
|
530
|
-
return null;
|
|
531
|
-
}
|
|
532
|
-
function getCompiledRegex(pattern, flags = "") {
|
|
533
|
-
if (flags && !/^[gimsuy]+$/.test(flags)) return null;
|
|
534
|
-
const key = `${pattern}\0${flags}`;
|
|
535
|
-
if (regexCache.has(key)) {
|
|
536
|
-
const cached = regexCache.get(key);
|
|
537
|
-
regexCache.delete(key);
|
|
538
|
-
regexCache.set(key, cached);
|
|
539
|
-
return cached;
|
|
540
|
-
}
|
|
541
|
-
if (validateRegex(pattern) !== null) return null;
|
|
542
|
-
try {
|
|
543
|
-
const re = new RegExp(pattern, flags);
|
|
544
|
-
if (regexCache.size >= REGEX_CACHE_MAX) {
|
|
545
|
-
const oldest = regexCache.keys().next().value;
|
|
546
|
-
if (oldest) regexCache.delete(oldest);
|
|
547
|
-
}
|
|
548
|
-
regexCache.set(key, re);
|
|
549
|
-
return re;
|
|
550
|
-
} catch {
|
|
551
|
-
return null;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
function matchesPattern(text, patterns) {
|
|
555
|
-
const p = Array.isArray(patterns) ? patterns : [patterns];
|
|
556
|
-
if (p.length === 0) return false;
|
|
557
|
-
const isMatch = pm(p, { nocase: true, dot: true });
|
|
558
|
-
const target = text.toLowerCase();
|
|
559
|
-
const directMatch = isMatch(target);
|
|
560
|
-
if (directMatch) return true;
|
|
561
|
-
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
562
|
-
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
563
|
-
}
|
|
564
|
-
function getNestedValue(obj, path11) {
|
|
565
|
-
if (!obj || typeof obj !== "object") return null;
|
|
566
|
-
const segments = path11.split(".");
|
|
567
|
-
for (const seg of segments) {
|
|
568
|
-
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
569
|
-
}
|
|
570
|
-
return segments.reduce((prev, curr) => prev?.[curr], obj);
|
|
571
|
-
}
|
|
572
|
-
function evaluateSmartConditions(args, rule) {
|
|
573
|
-
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
574
|
-
const mode = rule.conditionMode ?? "all";
|
|
575
|
-
const fieldCache = /* @__PURE__ */ new Map();
|
|
576
|
-
const resolveField = (field) => {
|
|
577
|
-
if (fieldCache.has(field)) return fieldCache.get(field) ?? null;
|
|
578
|
-
const rawVal = getNestedValue(args, field);
|
|
579
|
-
const rawStr = rawVal !== null && rawVal !== void 0 ? String(rawVal) : null;
|
|
580
|
-
const stripped = field === "command" && rawStr !== null ? normalizeCommandForPolicy(rawStr) : rawStr;
|
|
581
|
-
const val = stripped !== null ? stripped.replace(/\s+/g, " ").trim() : null;
|
|
582
|
-
fieldCache.set(field, val);
|
|
583
|
-
return val;
|
|
584
|
-
};
|
|
585
|
-
const results = rule.conditions.map((cond) => {
|
|
586
|
-
const val = resolveField(cond.field);
|
|
587
|
-
switch (cond.op) {
|
|
588
|
-
case "exists":
|
|
589
|
-
return val !== null && val !== "";
|
|
590
|
-
case "notExists":
|
|
591
|
-
return val === null || val === "";
|
|
592
|
-
case "contains":
|
|
593
|
-
return val !== null && cond.value ? val.includes(cond.value) : false;
|
|
594
|
-
case "notContains":
|
|
595
|
-
return val !== null && cond.value ? !val.includes(cond.value) : true;
|
|
596
|
-
case "matches": {
|
|
597
|
-
if (val === null || !cond.value) return false;
|
|
598
|
-
const reM = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
599
|
-
if (!reM) return false;
|
|
600
|
-
return reM.test(val);
|
|
601
|
-
}
|
|
602
|
-
case "notMatches": {
|
|
603
|
-
if (!cond.value) return false;
|
|
604
|
-
if (val === null) return true;
|
|
605
|
-
const reN = getCompiledRegex(cond.value, cond.flags ?? "");
|
|
606
|
-
if (!reN) return false;
|
|
607
|
-
return !reN.test(val);
|
|
608
|
-
}
|
|
609
|
-
case "matchesGlob":
|
|
610
|
-
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
611
|
-
case "notMatchesGlob":
|
|
612
|
-
return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
|
|
613
|
-
default:
|
|
614
|
-
return false;
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
return mode === "any" ? results.some((r) => r) : results.every((r) => r);
|
|
618
|
-
}
|
|
619
662
|
function isShieldVerdict(v) {
|
|
620
663
|
return v === "allow" || v === "review" || v === "block";
|
|
621
664
|
}
|
|
@@ -669,7 +712,7 @@ function assertBuiltinShieldRegexesAreSafe() {
|
|
|
669
712
|
}
|
|
670
713
|
}
|
|
671
714
|
}
|
|
672
|
-
var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS,
|
|
715
|
+
var ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, FS_OP_CACHE_MAX, fsOpCache, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, COST_PER_LOOP_ITER_USD, LONG_OUTPUT_THRESHOLD_BYTES;
|
|
673
716
|
var init_dist = __esm({
|
|
674
717
|
"packages/policy-engine/dist/index.mjs"() {
|
|
675
718
|
"use strict";
|
|
@@ -1159,6 +1202,10 @@ var init_dist = __esm({
|
|
|
1159
1202
|
MAX_DEPTH = 5;
|
|
1160
1203
|
MAX_STRING_BYTES = 1e5;
|
|
1161
1204
|
MAX_JSON_PARSE_BYTES = 1e4;
|
|
1205
|
+
MAX_REGEX_LENGTH = 256;
|
|
1206
|
+
REGEX_CACHE_MAX = 500;
|
|
1207
|
+
regexCache = /* @__PURE__ */ new Map();
|
|
1208
|
+
FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1162
1209
|
({ syntax } = mvdanSh);
|
|
1163
1210
|
sharedParser = syntax.NewParser();
|
|
1164
1211
|
MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -1192,9 +1239,34 @@ var init_dist = __esm({
|
|
|
1192
1239
|
"vi",
|
|
1193
1240
|
"emacs",
|
|
1194
1241
|
"code",
|
|
1195
|
-
"type"
|
|
1242
|
+
"type",
|
|
1243
|
+
// — the 22 that were missing —
|
|
1244
|
+
"grep",
|
|
1245
|
+
"egrep",
|
|
1246
|
+
"fgrep",
|
|
1247
|
+
"rg",
|
|
1248
|
+
"ag",
|
|
1249
|
+
"ack",
|
|
1250
|
+
"awk",
|
|
1251
|
+
"gawk",
|
|
1252
|
+
"sed",
|
|
1253
|
+
"cut",
|
|
1254
|
+
"tr",
|
|
1255
|
+
"jq",
|
|
1256
|
+
"yq",
|
|
1257
|
+
"od",
|
|
1258
|
+
"xxd",
|
|
1259
|
+
"hexdump",
|
|
1260
|
+
"strings",
|
|
1261
|
+
"sort",
|
|
1262
|
+
"uniq",
|
|
1263
|
+
"tac",
|
|
1264
|
+
"nl",
|
|
1265
|
+
"dd"
|
|
1196
1266
|
]);
|
|
1197
|
-
FS_OP_PRESCREEN_RE =
|
|
1267
|
+
FS_OP_PRESCREEN_RE = new RegExp(
|
|
1268
|
+
`(?:^|[\\s|;&(\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`
|
|
1269
|
+
);
|
|
1198
1270
|
HOME_CACHE_ALLOWLIST = [
|
|
1199
1271
|
".cache",
|
|
1200
1272
|
".npm/_npx",
|
|
@@ -1233,9 +1305,37 @@ var init_dist = __esm({
|
|
|
1233
1305
|
// for the canonical test-asserted contract.
|
|
1234
1306
|
rule: "shield:project-jail:block-read-env",
|
|
1235
1307
|
reason: "Reading .env files is blocked by project-jail shield",
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1308
|
+
// Structural, not a list. The previous form enumerated seven suffixes and
|
|
1309
|
+
// anchored on `$`, so `.env.prod`, `.env.ci` and `.env.local.bak` — all
|
|
1310
|
+
// gitignored, all routinely holding real secrets — were never covered. A
|
|
1311
|
+
// hand-written list of what to protect is only ever as complete as the day
|
|
1312
|
+
// it was typed; this says "`.env` plus any suffix chain" and then names the
|
|
1313
|
+
// exceptions, which is the direction that fails safe.
|
|
1314
|
+
//
|
|
1315
|
+
// \.env the segment itself
|
|
1316
|
+
// (?![\w-]) a boundary, so `.environment` and `.envrc` are NOT .env
|
|
1317
|
+
// files. Without it a flat suffix class swallows both.
|
|
1318
|
+
// (?![\w-]) a boundary, so `.environment` and `.envrc` are NOT .env
|
|
1319
|
+
// [\w.-]*$ any suffix chain. Flat class, no nested quantifier —
|
|
1320
|
+
// `(\.[\w-]+)*` reads the same but is rejected by
|
|
1321
|
+
// safe-regex2, and this pattern runs on the hook hot path.
|
|
1322
|
+
//
|
|
1323
|
+
// The two exclusions are NOT the same shape, because the words do not mean
|
|
1324
|
+
// the same thing:
|
|
1325
|
+
//
|
|
1326
|
+
// (?!\.(?:example|sample|template)\b) — "this file is a fixture", and it
|
|
1327
|
+
// stays a fixture whatever follows, so `.env.example.md` is allowed too.
|
|
1328
|
+
// These are checked into git by convention: already public, so blocking
|
|
1329
|
+
// them buys nothing and costs the most common legitimate agent read.
|
|
1330
|
+
//
|
|
1331
|
+
// (?!\.test$) — anchored, because `test` names an ENVIRONMENT, not a
|
|
1332
|
+
// fixture. `.env.test` is the committed template and stays allowed, but
|
|
1333
|
+
// `.env.test.local` is gitignored by the `.env*.local` convention and
|
|
1334
|
+
// holds real values, so it must block. Using `\b` here — the obvious
|
|
1335
|
+
// symmetry — silently exempts every `.env.test.*` file.
|
|
1336
|
+
//
|
|
1337
|
+
// shields.test.ts:983-995 is the canonical contract; keep both in step.
|
|
1338
|
+
match: (p) => /(?:^|[\\/])\.env(?![\w-])(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$/i.test(p)
|
|
1239
1339
|
},
|
|
1240
1340
|
{
|
|
1241
1341
|
// verdict: 'review' (not 'block') is a deliberate design choice
|
|
@@ -1269,6 +1369,13 @@ var init_dist = __esm({
|
|
|
1269
1369
|
)
|
|
1270
1370
|
}
|
|
1271
1371
|
];
|
|
1372
|
+
BASH_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1373
|
+
"bash",
|
|
1374
|
+
"execute_bash",
|
|
1375
|
+
"run_shell_command",
|
|
1376
|
+
"shell",
|
|
1377
|
+
"exec_command"
|
|
1378
|
+
]);
|
|
1272
1379
|
AST_FS_REGEX_RULES = /* @__PURE__ */ new Set([
|
|
1273
1380
|
"block-rm-rf-home",
|
|
1274
1381
|
"shield:project-jail:block-read-ssh",
|
|
@@ -1292,10 +1399,6 @@ var init_dist = __esm({
|
|
|
1292
1399
|
deriveRedirOp("cat <<X\nX"),
|
|
1293
1400
|
deriveRedirOp("cat <<-X\nX")
|
|
1294
1401
|
]);
|
|
1295
|
-
MAX_REGEX_LENGTH = 100;
|
|
1296
|
-
REGEX_CACHE_MAX = 500;
|
|
1297
|
-
regexCache = /* @__PURE__ */ new Map();
|
|
1298
|
-
FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1299
1402
|
aws_default = {
|
|
1300
1403
|
name: "aws",
|
|
1301
1404
|
description: "Protects AWS infrastructure from destructive AI operations",
|
|
@@ -2461,6 +2564,7 @@ var init_managed = __esm({
|
|
|
2461
2564
|
var init_build = __esm({
|
|
2462
2565
|
"src/shields/build.ts"() {
|
|
2463
2566
|
"use strict";
|
|
2567
|
+
init_dist();
|
|
2464
2568
|
}
|
|
2465
2569
|
});
|
|
2466
2570
|
|
|
@@ -2493,8 +2597,14 @@ var init_config = __esm({
|
|
|
2493
2597
|
settings: {
|
|
2494
2598
|
mode: "standard",
|
|
2495
2599
|
autoStartDaemon: true,
|
|
2496
|
-
|
|
2497
|
-
//
|
|
2600
|
+
// OFF by default. The snapshot store is a per-project bare git repo with
|
|
2601
|
+
// no size ceiling, and eviction drops the index row without deleting the
|
|
2602
|
+
// objects — on one machine it reached 378G (352G of it orphaned tmp_pack_*
|
|
2603
|
+
// from interrupted `git gc`) and filled the disk. A security tool must not
|
|
2604
|
+
// be what fills a customer's disk. Re-enable per install with
|
|
2605
|
+
// `{"settings":{"enableUndo":true}}`; the default flips back when the
|
|
2606
|
+
// bounded copy-store lands (doc/undo-v2-copy-store-design.md).
|
|
2607
|
+
enableUndo: false,
|
|
2498
2608
|
enableHookLogDebug: true,
|
|
2499
2609
|
approvalTimeoutMs: 12e4,
|
|
2500
2610
|
// 120-second auto-deny timeout
|
|
@@ -2693,7 +2803,8 @@ var init_config = __esm({
|
|
|
2693
2803
|
skillPinning: { enabled: false, mode: "warn", roots: [] },
|
|
2694
2804
|
trustedHosts: [],
|
|
2695
2805
|
trustedHostsManaged: false,
|
|
2696
|
-
appPermissions: {}
|
|
2806
|
+
appPermissions: {},
|
|
2807
|
+
managedJailPaths: []
|
|
2697
2808
|
},
|
|
2698
2809
|
environments: {}
|
|
2699
2810
|
};
|
|
@@ -3034,6 +3145,7 @@ var init_scan_watermark = __esm({
|
|
|
3034
3145
|
"src/daemon/scan-watermark.ts"() {
|
|
3035
3146
|
"use strict";
|
|
3036
3147
|
init_dlp();
|
|
3148
|
+
init_config();
|
|
3037
3149
|
init_dist();
|
|
3038
3150
|
MAX_LINE_BYTES = 2 * 1024 * 1024;
|
|
3039
3151
|
}
|
|
@@ -3813,6 +3925,7 @@ var init_policy = __esm({
|
|
|
3813
3925
|
init_trusted_hosts();
|
|
3814
3926
|
init_dist();
|
|
3815
3927
|
init_dist();
|
|
3928
|
+
init_dist();
|
|
3816
3929
|
}
|
|
3817
3930
|
});
|
|
3818
3931
|
|
|
@@ -4187,7 +4300,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
|
|
|
4187
4300
|
}
|
|
4188
4301
|
}
|
|
4189
4302
|
let astFsMatched = false;
|
|
4190
|
-
const astRanForBash = toolNameLower
|
|
4303
|
+
const astRanForBash = isShellShapedTool(toolNameLower, toolInspectionMap);
|
|
4191
4304
|
if (astRanForBash) {
|
|
4192
4305
|
astFsMatched = pushFsOpAstFinding(
|
|
4193
4306
|
String(input.command ?? ""),
|
|
@@ -4205,7 +4318,7 @@ function processClaudeFile(file, projPath, projLabel, ruleSources, startDate, re
|
|
|
4205
4318
|
for (const source of ruleSources) {
|
|
4206
4319
|
const { rule } = source;
|
|
4207
4320
|
if (rule.verdict === "allow") continue;
|
|
4208
|
-
if (
|
|
4321
|
+
if (!toolMatchesRule(toolNameLower, rule.tool, toolInspectionMap)) continue;
|
|
4209
4322
|
if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
|
|
4210
4323
|
if (!evaluateSmartConditions(input, rule)) continue;
|
|
4211
4324
|
const inputPreview = preview(input, 120);
|
|
@@ -4487,7 +4600,7 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
|
|
|
4487
4600
|
}
|
|
4488
4601
|
}
|
|
4489
4602
|
let astFsMatched = false;
|
|
4490
|
-
const astRanForBash = toolNameLower
|
|
4603
|
+
const astRanForBash = isShellShapedTool(toolNameLower, toolInspectionMap);
|
|
4491
4604
|
if (astRanForBash) {
|
|
4492
4605
|
astFsMatched = pushFsOpAstFinding(
|
|
4493
4606
|
String(input.command ?? ""),
|
|
@@ -4505,7 +4618,7 @@ function scanGeminiHistory(startDate, onProgress, onLine) {
|
|
|
4505
4618
|
for (const source of ruleSources) {
|
|
4506
4619
|
const { rule } = source;
|
|
4507
4620
|
if (rule.verdict === "allow") continue;
|
|
4508
|
-
if (
|
|
4621
|
+
if (!toolMatchesRule(toolNameLower, rule.tool, toolInspectionMap)) continue;
|
|
4509
4622
|
if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
|
|
4510
4623
|
if (!evaluateSmartConditions(input, rule)) continue;
|
|
4511
4624
|
const inputPreview = preview(input, 120);
|
|
@@ -4727,7 +4840,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
|
|
|
4727
4840
|
}
|
|
4728
4841
|
}
|
|
4729
4842
|
let astFsMatched = false;
|
|
4730
|
-
const astRanForBash = toolNameLower
|
|
4843
|
+
const astRanForBash = isShellShapedTool(toolNameLower, toolInspectionMap);
|
|
4731
4844
|
if (astRanForBash) {
|
|
4732
4845
|
astFsMatched = pushFsOpAstFinding(
|
|
4733
4846
|
String(input["command"] ?? ""),
|
|
@@ -4745,8 +4858,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
|
|
|
4745
4858
|
for (const source of ruleSources) {
|
|
4746
4859
|
const { rule } = source;
|
|
4747
4860
|
if (rule.verdict === "allow") continue;
|
|
4748
|
-
if (rule.tool && !
|
|
4749
|
-
continue;
|
|
4861
|
+
if (rule.tool && !toolMatchesRule(toolNameLower, rule.tool, toolInspectionMap)) continue;
|
|
4750
4862
|
if (astRanForBash && rule.name && AST_FS_REGEX_RULES.has(rule.name)) continue;
|
|
4751
4863
|
if (!evaluateSmartConditions(input, rule)) continue;
|
|
4752
4864
|
const inputPreview = preview(input, 120);
|
|
@@ -4807,7 +4919,7 @@ function scanCodexHistory(startDate, onProgress, onLine) {
|
|
|
4807
4919
|
}
|
|
4808
4920
|
return result;
|
|
4809
4921
|
}
|
|
4810
|
-
var CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS;
|
|
4922
|
+
var toolInspectionMap, CODE_EXTENSIONS, SELF_OUTPUT_MARKERS, FIXTURE_TOKEN_PATTERNS, TERMINAL_ESCAPE_RE, LOOP_TOOLS, LOOP_THRESHOLD, LOOP_TIMESPAN_THRESHOLD_MS;
|
|
4811
4923
|
var init_scan = __esm({
|
|
4812
4924
|
"src/cli/commands/scan.ts"() {
|
|
4813
4925
|
"use strict";
|
|
@@ -4828,6 +4940,7 @@ var init_scan = __esm({
|
|
|
4828
4940
|
init_protection();
|
|
4829
4941
|
init_scan_json();
|
|
4830
4942
|
init_scan_history();
|
|
4943
|
+
toolInspectionMap = DEFAULT_CONFIG.policy.toolInspection;
|
|
4831
4944
|
CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4832
4945
|
".ts",
|
|
4833
4946
|
".tsx",
|