@node9/proxy 2.13.1 → 2.14.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/dist/cli.js +262 -26
- package/dist/cli.mjs +262 -26
- package/dist/dashboard.mjs +246 -24
- package/dist/index.js +219 -21
- package/dist/index.mjs +219 -21
- package/dist/scan-ink.mjs +44 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1700,12 +1700,56 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1700
1700
|
"nl",
|
|
1701
1701
|
"dd"
|
|
1702
1702
|
]);
|
|
1703
|
+
var SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
|
|
1704
|
+
var RSYNC_SKIP = [
|
|
1705
|
+
"e",
|
|
1706
|
+
"--rsh",
|
|
1707
|
+
"--exclude",
|
|
1708
|
+
"--exclude-from",
|
|
1709
|
+
"--include",
|
|
1710
|
+
"--include-from",
|
|
1711
|
+
"--files-from",
|
|
1712
|
+
"f",
|
|
1713
|
+
"--filter"
|
|
1714
|
+
];
|
|
1715
|
+
var COPY_VERBS = {
|
|
1716
|
+
cp: { source: "allButLast", targetDirFlag: true },
|
|
1717
|
+
mv: { source: "allButLast", targetDirFlag: true },
|
|
1718
|
+
install: { source: "allButLast", targetDirFlag: true },
|
|
1719
|
+
ln: { source: "first", targetDirFlag: true },
|
|
1720
|
+
scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
|
|
1721
|
+
rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
|
|
1722
|
+
tar: {
|
|
1723
|
+
source: "archive",
|
|
1724
|
+
archive: "tar",
|
|
1725
|
+
skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
|
|
1726
|
+
},
|
|
1727
|
+
zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
|
|
1728
|
+
ar: { source: "archive", archive: "ar" },
|
|
1729
|
+
"7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
|
|
1730
|
+
gzip: { source: "all" },
|
|
1731
|
+
bzip2: { source: "all" },
|
|
1732
|
+
xz: { source: "all" },
|
|
1733
|
+
"docker cp": { source: "allButLast" },
|
|
1734
|
+
"kubectl cp": { source: "allButLast" },
|
|
1735
|
+
"gsutil cp": { source: "allButLast" },
|
|
1736
|
+
"gsutil rsync": { source: "allButLast" },
|
|
1737
|
+
"rclone copy": { source: "allButLast" },
|
|
1738
|
+
"rclone sync": { source: "allButLast" },
|
|
1739
|
+
"aws s3 cp": { source: "allButLast" },
|
|
1740
|
+
"aws s3 mv": { source: "allButLast" },
|
|
1741
|
+
"aws s3 sync": { source: "allButLast" },
|
|
1742
|
+
"gcloud storage cp": { source: "allButLast" },
|
|
1743
|
+
"az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
|
|
1744
|
+
};
|
|
1745
|
+
var TAR_MODE_WORD = /^[a-zA-Z]+$/;
|
|
1746
|
+
var COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
|
|
1703
1747
|
var FS_OP_PRESCREEN_RE = new RegExp(
|
|
1704
1748
|
// A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
|
|
1705
1749
|
// reader right after `"` / `'`, and without these two characters the
|
|
1706
1750
|
// prescreen rejected every string-wrapped read before the parser ran.
|
|
1707
1751
|
// Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
|
|
1708
|
-
`(?:^|[\\s|;&("'\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
1752
|
+
`(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
1709
1753
|
);
|
|
1710
1754
|
var HOME_CACHE_ALLOWLIST = [
|
|
1711
1755
|
".cache",
|
|
@@ -2140,20 +2184,32 @@ function isProtectedHomePath(rawPath) {
|
|
|
2140
2184
|
}
|
|
2141
2185
|
return true;
|
|
2142
2186
|
}
|
|
2143
|
-
function
|
|
2144
|
-
const
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
const name = (words[0] ?? "").toLowerCase();
|
|
2148
|
-
const flags = [];
|
|
2149
|
-
const paths = [];
|
|
2150
|
-
for (let i = 1; i < words.length; i++) {
|
|
2187
|
+
function positionedArgs(words, from = 1, to = words.length) {
|
|
2188
|
+
const out = [];
|
|
2189
|
+
let afterFlag = null;
|
|
2190
|
+
for (let i = from; i < to; i++) {
|
|
2151
2191
|
const v = words[i];
|
|
2152
|
-
if (v === null)
|
|
2153
|
-
|
|
2154
|
-
|
|
2192
|
+
if (v === null) {
|
|
2193
|
+
afterFlag = null;
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2196
|
+
if (v.startsWith("-")) {
|
|
2197
|
+
afterFlag = v;
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
out.push({ value: v, index: out.length, argv: i, afterFlag });
|
|
2201
|
+
afterFlag = null;
|
|
2155
2202
|
}
|
|
2156
|
-
return
|
|
2203
|
+
return out;
|
|
2204
|
+
}
|
|
2205
|
+
function extractLiteralArgs(callExpr) {
|
|
2206
|
+
const rawArgs = callExpr.Args || [];
|
|
2207
|
+
if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
|
|
2208
|
+
const words = rawArgs.map((a) => resolveWordLiteral(a));
|
|
2209
|
+
const name = baseWord(words[0]);
|
|
2210
|
+
const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
|
|
2211
|
+
const args = positionedArgs(words);
|
|
2212
|
+
return { name, flags, paths: args.map((a) => a.value), words, args };
|
|
2157
2213
|
}
|
|
2158
2214
|
var NET_BINARIES = /* @__PURE__ */ new Set([
|
|
2159
2215
|
"curl",
|
|
@@ -2577,6 +2633,9 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2577
2633
|
if (result?.verdict === "block") return false;
|
|
2578
2634
|
}
|
|
2579
2635
|
}
|
|
2636
|
+
for (const p of copySourcePaths(words)) {
|
|
2637
|
+
result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
|
|
2638
|
+
}
|
|
2580
2639
|
return true;
|
|
2581
2640
|
});
|
|
2582
2641
|
return result;
|
|
@@ -2589,6 +2648,140 @@ function stricter(a, b) {
|
|
|
2589
2648
|
if (!b) return a;
|
|
2590
2649
|
return b.verdict === "block" && a.verdict !== "block" ? b : a;
|
|
2591
2650
|
}
|
|
2651
|
+
function flagInfo(w) {
|
|
2652
|
+
if (w.startsWith("--")) {
|
|
2653
|
+
const eq = w.indexOf("=");
|
|
2654
|
+
return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
|
|
2655
|
+
}
|
|
2656
|
+
const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
|
|
2657
|
+
if (!m) return { letter: null, long: null, attached: null };
|
|
2658
|
+
return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
|
|
2659
|
+
}
|
|
2660
|
+
function flagIs(w, names) {
|
|
2661
|
+
if (w === null) return false;
|
|
2662
|
+
const f = flagInfo(w);
|
|
2663
|
+
return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
|
|
2664
|
+
}
|
|
2665
|
+
function operandOf(a, names) {
|
|
2666
|
+
if (!names || a.afterFlag === null) return false;
|
|
2667
|
+
return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
|
|
2668
|
+
}
|
|
2669
|
+
function resolveCopyShape(words, h) {
|
|
2670
|
+
const verb = baseWord(words[h]);
|
|
2671
|
+
if (!verb) return null;
|
|
2672
|
+
const direct = COPY_VERBS[verb];
|
|
2673
|
+
if (direct) return { shape: direct, last: h };
|
|
2674
|
+
const slots = positionedArgs(words, h + 1);
|
|
2675
|
+
for (let i = 0; i < slots.length; i++) {
|
|
2676
|
+
for (let n = 3; n >= 1; n--) {
|
|
2677
|
+
const part = slots.slice(i, i + n);
|
|
2678
|
+
if (part.length < n) continue;
|
|
2679
|
+
const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
|
|
2680
|
+
const shape = COPY_VERBS[key];
|
|
2681
|
+
if (shape) return { shape, last: part[n - 1].argv };
|
|
2682
|
+
}
|
|
2683
|
+
if (slots[i].afterFlag === null) return null;
|
|
2684
|
+
}
|
|
2685
|
+
return null;
|
|
2686
|
+
}
|
|
2687
|
+
var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
|
|
2688
|
+
function findStartPoints(words, h) {
|
|
2689
|
+
const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
|
|
2690
|
+
if (k < 0) return { k, starts: [] };
|
|
2691
|
+
const firstPredicate = words.findIndex(
|
|
2692
|
+
(w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
|
|
2693
|
+
);
|
|
2694
|
+
const end = firstPredicate > h ? firstPredicate : k;
|
|
2695
|
+
return { k, starts: positionalAfter(words, h + 1, end) };
|
|
2696
|
+
}
|
|
2697
|
+
function copySourcePaths(words) {
|
|
2698
|
+
const h = unwrapCommandHead(words);
|
|
2699
|
+
const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
|
|
2700
|
+
if (fi >= 0) {
|
|
2701
|
+
const { k, starts } = findStartPoints(words, fi);
|
|
2702
|
+
if (k < 0) return [];
|
|
2703
|
+
const action = unwrapCommandHead(words.slice(k + 1));
|
|
2704
|
+
return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
|
|
2705
|
+
}
|
|
2706
|
+
if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
|
|
2707
|
+
const r = resolveCopyShape(words, h);
|
|
2708
|
+
if (!r) return [];
|
|
2709
|
+
const { shape, last } = r;
|
|
2710
|
+
const args = positionedArgs(words, last + 1);
|
|
2711
|
+
const tail = words.slice(last + 1);
|
|
2712
|
+
const skipped = (a) => operandOf(a, shape.skipFlags);
|
|
2713
|
+
const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
|
|
2714
|
+
const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
|
|
2715
|
+
const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
|
|
2716
|
+
const dynamicDest = lastOperand === null;
|
|
2717
|
+
const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
|
|
2718
|
+
if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
|
|
2719
|
+
return [];
|
|
2720
|
+
let src;
|
|
2721
|
+
switch (shape.source) {
|
|
2722
|
+
case "all":
|
|
2723
|
+
src = args;
|
|
2724
|
+
break;
|
|
2725
|
+
case "first":
|
|
2726
|
+
src = targetDir ? args : args.slice(0, 1);
|
|
2727
|
+
break;
|
|
2728
|
+
case "flagOperand": {
|
|
2729
|
+
const inline = tail.filter((w) => w !== null && w.startsWith("--")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && (shape.sourceFlags ?? []).includes(f.long ?? "")).map((f) => f.attached);
|
|
2730
|
+
return [
|
|
2731
|
+
...args.filter(
|
|
2732
|
+
(a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
|
|
2733
|
+
).map((a) => a.value),
|
|
2734
|
+
...inline
|
|
2735
|
+
];
|
|
2736
|
+
}
|
|
2737
|
+
case "archive":
|
|
2738
|
+
src = archiveInputs(shape.archive, args, tail);
|
|
2739
|
+
break;
|
|
2740
|
+
case "allButLast":
|
|
2741
|
+
src = targetDir || dynamicDest ? args : args.slice(0, -1);
|
|
2742
|
+
break;
|
|
2743
|
+
}
|
|
2744
|
+
return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
|
|
2745
|
+
}
|
|
2746
|
+
function archiveInputs(kind, args, tail) {
|
|
2747
|
+
const first = args[0];
|
|
2748
|
+
const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
|
|
2749
|
+
if (kind === "tar") {
|
|
2750
|
+
const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
|
|
2751
|
+
const mode = (bareKey ? first.value : "") + flagsText;
|
|
2752
|
+
const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
|
|
2753
|
+
const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
|
|
2754
|
+
if (extracting && !writing) return [];
|
|
2755
|
+
void mode;
|
|
2756
|
+
let i = 0;
|
|
2757
|
+
if (bareKey) {
|
|
2758
|
+
i = 1;
|
|
2759
|
+
const next = args[1];
|
|
2760
|
+
if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
|
|
2761
|
+
}
|
|
2762
|
+
return args.slice(i);
|
|
2763
|
+
}
|
|
2764
|
+
if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
|
|
2765
|
+
if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
|
|
2766
|
+
return args.slice(2);
|
|
2767
|
+
}
|
|
2768
|
+
var COPY_RULE_OF = {
|
|
2769
|
+
"shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
|
|
2770
|
+
"shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
|
|
2771
|
+
"shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
|
|
2772
|
+
"shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
|
|
2773
|
+
};
|
|
2774
|
+
function copyVerdictOf(hit) {
|
|
2775
|
+
if (!hit) return null;
|
|
2776
|
+
const ruleName = COPY_RULE_OF[hit.ruleName];
|
|
2777
|
+
if (!ruleName) return null;
|
|
2778
|
+
return {
|
|
2779
|
+
ruleName,
|
|
2780
|
+
verdict: "review",
|
|
2781
|
+
reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
|
|
2782
|
+
path: hit.path
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2592
2785
|
function matchSensitivePath2(p) {
|
|
2593
2786
|
for (const sp of SENSITIVE_PATH_RULES) {
|
|
2594
2787
|
if (sp.match(p))
|
|
@@ -2596,14 +2789,15 @@ function matchSensitivePath2(p) {
|
|
|
2596
2789
|
}
|
|
2597
2790
|
return null;
|
|
2598
2791
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2792
|
+
function baseWord(w) {
|
|
2793
|
+
return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
|
|
2794
|
+
}
|
|
2795
|
+
var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
|
|
2796
|
+
var positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
|
|
2601
2797
|
function wrappedReadPaths(words, name) {
|
|
2602
2798
|
if (name === "find") {
|
|
2603
|
-
const k = words
|
|
2604
|
-
|
|
2605
|
-
const firstPredicate = words.findIndex((w, i) => i > 0 && w !== null && w.startsWith("-"));
|
|
2606
|
-
return positionalAfter(words, 1, firstPredicate > 0 ? firstPredicate : k);
|
|
2799
|
+
const { k, starts } = findStartPoints(words, 0);
|
|
2800
|
+
return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
|
|
2607
2801
|
}
|
|
2608
2802
|
if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
|
|
2609
2803
|
const h = unwrapCommandHead(words);
|
|
@@ -3464,6 +3658,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3464
3658
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
3465
3659
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
3466
3660
|
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
3661
|
+
let pendingAstReview;
|
|
3467
3662
|
if (bashCommand !== null) {
|
|
3468
3663
|
const pipeVerdict = pipeChainVerdict(
|
|
3469
3664
|
bashCommand,
|
|
@@ -3475,7 +3670,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3475
3670
|
if (fsVerdict) {
|
|
3476
3671
|
const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
|
|
3477
3672
|
const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
|
|
3478
|
-
|
|
3673
|
+
const astVerdict = {
|
|
3479
3674
|
decision: fsVerdict.verdict,
|
|
3480
3675
|
blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
|
|
3481
3676
|
reason: fsVerdict.reason,
|
|
@@ -3483,6 +3678,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3483
3678
|
ruleName: fsVerdict.ruleName,
|
|
3484
3679
|
ruleDescription: fsVerdict.reason
|
|
3485
3680
|
};
|
|
3681
|
+
if (fsVerdict.verdict === "block") return astVerdict;
|
|
3682
|
+
pendingAstReview = astVerdict;
|
|
3486
3683
|
}
|
|
3487
3684
|
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
3488
3685
|
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
@@ -3525,7 +3722,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3525
3722
|
const matchedRule = resolvePinned(matches);
|
|
3526
3723
|
if (matchedRule) {
|
|
3527
3724
|
if (matchedRule.verdict === "allow")
|
|
3528
|
-
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
3725
|
+
return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
3529
3726
|
return {
|
|
3530
3727
|
decision: matchedRule.verdict,
|
|
3531
3728
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -3552,6 +3749,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3552
3749
|
allTokens = analyzed.allTokens;
|
|
3553
3750
|
pathTokens = analyzed.paths;
|
|
3554
3751
|
const candidates = [];
|
|
3752
|
+
if (pendingAstReview) candidates.push(pendingAstReview);
|
|
3555
3753
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
3556
3754
|
if (evalVerdict === "block") {
|
|
3557
3755
|
return {
|
package/dist/index.mjs
CHANGED
|
@@ -1670,12 +1670,56 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1670
1670
|
"nl",
|
|
1671
1671
|
"dd"
|
|
1672
1672
|
]);
|
|
1673
|
+
var SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
|
|
1674
|
+
var RSYNC_SKIP = [
|
|
1675
|
+
"e",
|
|
1676
|
+
"--rsh",
|
|
1677
|
+
"--exclude",
|
|
1678
|
+
"--exclude-from",
|
|
1679
|
+
"--include",
|
|
1680
|
+
"--include-from",
|
|
1681
|
+
"--files-from",
|
|
1682
|
+
"f",
|
|
1683
|
+
"--filter"
|
|
1684
|
+
];
|
|
1685
|
+
var COPY_VERBS = {
|
|
1686
|
+
cp: { source: "allButLast", targetDirFlag: true },
|
|
1687
|
+
mv: { source: "allButLast", targetDirFlag: true },
|
|
1688
|
+
install: { source: "allButLast", targetDirFlag: true },
|
|
1689
|
+
ln: { source: "first", targetDirFlag: true },
|
|
1690
|
+
scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
|
|
1691
|
+
rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
|
|
1692
|
+
tar: {
|
|
1693
|
+
source: "archive",
|
|
1694
|
+
archive: "tar",
|
|
1695
|
+
skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
|
|
1696
|
+
},
|
|
1697
|
+
zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
|
|
1698
|
+
ar: { source: "archive", archive: "ar" },
|
|
1699
|
+
"7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
|
|
1700
|
+
gzip: { source: "all" },
|
|
1701
|
+
bzip2: { source: "all" },
|
|
1702
|
+
xz: { source: "all" },
|
|
1703
|
+
"docker cp": { source: "allButLast" },
|
|
1704
|
+
"kubectl cp": { source: "allButLast" },
|
|
1705
|
+
"gsutil cp": { source: "allButLast" },
|
|
1706
|
+
"gsutil rsync": { source: "allButLast" },
|
|
1707
|
+
"rclone copy": { source: "allButLast" },
|
|
1708
|
+
"rclone sync": { source: "allButLast" },
|
|
1709
|
+
"aws s3 cp": { source: "allButLast" },
|
|
1710
|
+
"aws s3 mv": { source: "allButLast" },
|
|
1711
|
+
"aws s3 sync": { source: "allButLast" },
|
|
1712
|
+
"gcloud storage cp": { source: "allButLast" },
|
|
1713
|
+
"az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
|
|
1714
|
+
};
|
|
1715
|
+
var TAR_MODE_WORD = /^[a-zA-Z]+$/;
|
|
1716
|
+
var COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
|
|
1673
1717
|
var FS_OP_PRESCREEN_RE = new RegExp(
|
|
1674
1718
|
// A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
|
|
1675
1719
|
// reader right after `"` / `'`, and without these two characters the
|
|
1676
1720
|
// prescreen rejected every string-wrapped read before the parser ran.
|
|
1677
1721
|
// Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
|
|
1678
|
-
`(?:^|[\\s|;&("'\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
1722
|
+
`(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
1679
1723
|
);
|
|
1680
1724
|
var HOME_CACHE_ALLOWLIST = [
|
|
1681
1725
|
".cache",
|
|
@@ -2110,20 +2154,32 @@ function isProtectedHomePath(rawPath) {
|
|
|
2110
2154
|
}
|
|
2111
2155
|
return true;
|
|
2112
2156
|
}
|
|
2113
|
-
function
|
|
2114
|
-
const
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
const name = (words[0] ?? "").toLowerCase();
|
|
2118
|
-
const flags = [];
|
|
2119
|
-
const paths = [];
|
|
2120
|
-
for (let i = 1; i < words.length; i++) {
|
|
2157
|
+
function positionedArgs(words, from = 1, to = words.length) {
|
|
2158
|
+
const out = [];
|
|
2159
|
+
let afterFlag = null;
|
|
2160
|
+
for (let i = from; i < to; i++) {
|
|
2121
2161
|
const v = words[i];
|
|
2122
|
-
if (v === null)
|
|
2123
|
-
|
|
2124
|
-
|
|
2162
|
+
if (v === null) {
|
|
2163
|
+
afterFlag = null;
|
|
2164
|
+
continue;
|
|
2165
|
+
}
|
|
2166
|
+
if (v.startsWith("-")) {
|
|
2167
|
+
afterFlag = v;
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
out.push({ value: v, index: out.length, argv: i, afterFlag });
|
|
2171
|
+
afterFlag = null;
|
|
2125
2172
|
}
|
|
2126
|
-
return
|
|
2173
|
+
return out;
|
|
2174
|
+
}
|
|
2175
|
+
function extractLiteralArgs(callExpr) {
|
|
2176
|
+
const rawArgs = callExpr.Args || [];
|
|
2177
|
+
if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
|
|
2178
|
+
const words = rawArgs.map((a) => resolveWordLiteral(a));
|
|
2179
|
+
const name = baseWord(words[0]);
|
|
2180
|
+
const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
|
|
2181
|
+
const args = positionedArgs(words);
|
|
2182
|
+
return { name, flags, paths: args.map((a) => a.value), words, args };
|
|
2127
2183
|
}
|
|
2128
2184
|
var NET_BINARIES = /* @__PURE__ */ new Set([
|
|
2129
2185
|
"curl",
|
|
@@ -2547,6 +2603,9 @@ function analyzeFsOperationImpl(command, depth = 0) {
|
|
|
2547
2603
|
if (result?.verdict === "block") return false;
|
|
2548
2604
|
}
|
|
2549
2605
|
}
|
|
2606
|
+
for (const p of copySourcePaths(words)) {
|
|
2607
|
+
result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
|
|
2608
|
+
}
|
|
2550
2609
|
return true;
|
|
2551
2610
|
});
|
|
2552
2611
|
return result;
|
|
@@ -2559,6 +2618,140 @@ function stricter(a, b) {
|
|
|
2559
2618
|
if (!b) return a;
|
|
2560
2619
|
return b.verdict === "block" && a.verdict !== "block" ? b : a;
|
|
2561
2620
|
}
|
|
2621
|
+
function flagInfo(w) {
|
|
2622
|
+
if (w.startsWith("--")) {
|
|
2623
|
+
const eq = w.indexOf("=");
|
|
2624
|
+
return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
|
|
2625
|
+
}
|
|
2626
|
+
const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
|
|
2627
|
+
if (!m) return { letter: null, long: null, attached: null };
|
|
2628
|
+
return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
|
|
2629
|
+
}
|
|
2630
|
+
function flagIs(w, names) {
|
|
2631
|
+
if (w === null) return false;
|
|
2632
|
+
const f = flagInfo(w);
|
|
2633
|
+
return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
|
|
2634
|
+
}
|
|
2635
|
+
function operandOf(a, names) {
|
|
2636
|
+
if (!names || a.afterFlag === null) return false;
|
|
2637
|
+
return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
|
|
2638
|
+
}
|
|
2639
|
+
function resolveCopyShape(words, h) {
|
|
2640
|
+
const verb = baseWord(words[h]);
|
|
2641
|
+
if (!verb) return null;
|
|
2642
|
+
const direct = COPY_VERBS[verb];
|
|
2643
|
+
if (direct) return { shape: direct, last: h };
|
|
2644
|
+
const slots = positionedArgs(words, h + 1);
|
|
2645
|
+
for (let i = 0; i < slots.length; i++) {
|
|
2646
|
+
for (let n = 3; n >= 1; n--) {
|
|
2647
|
+
const part = slots.slice(i, i + n);
|
|
2648
|
+
if (part.length < n) continue;
|
|
2649
|
+
const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
|
|
2650
|
+
const shape = COPY_VERBS[key];
|
|
2651
|
+
if (shape) return { shape, last: part[n - 1].argv };
|
|
2652
|
+
}
|
|
2653
|
+
if (slots[i].afterFlag === null) return null;
|
|
2654
|
+
}
|
|
2655
|
+
return null;
|
|
2656
|
+
}
|
|
2657
|
+
var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
|
|
2658
|
+
function findStartPoints(words, h) {
|
|
2659
|
+
const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
|
|
2660
|
+
if (k < 0) return { k, starts: [] };
|
|
2661
|
+
const firstPredicate = words.findIndex(
|
|
2662
|
+
(w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
|
|
2663
|
+
);
|
|
2664
|
+
const end = firstPredicate > h ? firstPredicate : k;
|
|
2665
|
+
return { k, starts: positionalAfter(words, h + 1, end) };
|
|
2666
|
+
}
|
|
2667
|
+
function copySourcePaths(words) {
|
|
2668
|
+
const h = unwrapCommandHead(words);
|
|
2669
|
+
const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
|
|
2670
|
+
if (fi >= 0) {
|
|
2671
|
+
const { k, starts } = findStartPoints(words, fi);
|
|
2672
|
+
if (k < 0) return [];
|
|
2673
|
+
const action = unwrapCommandHead(words.slice(k + 1));
|
|
2674
|
+
return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
|
|
2675
|
+
}
|
|
2676
|
+
if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
|
|
2677
|
+
const r = resolveCopyShape(words, h);
|
|
2678
|
+
if (!r) return [];
|
|
2679
|
+
const { shape, last } = r;
|
|
2680
|
+
const args = positionedArgs(words, last + 1);
|
|
2681
|
+
const tail = words.slice(last + 1);
|
|
2682
|
+
const skipped = (a) => operandOf(a, shape.skipFlags);
|
|
2683
|
+
const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
|
|
2684
|
+
const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
|
|
2685
|
+
const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
|
|
2686
|
+
const dynamicDest = lastOperand === null;
|
|
2687
|
+
const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
|
|
2688
|
+
if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
|
|
2689
|
+
return [];
|
|
2690
|
+
let src;
|
|
2691
|
+
switch (shape.source) {
|
|
2692
|
+
case "all":
|
|
2693
|
+
src = args;
|
|
2694
|
+
break;
|
|
2695
|
+
case "first":
|
|
2696
|
+
src = targetDir ? args : args.slice(0, 1);
|
|
2697
|
+
break;
|
|
2698
|
+
case "flagOperand": {
|
|
2699
|
+
const inline = tail.filter((w) => w !== null && w.startsWith("--")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && (shape.sourceFlags ?? []).includes(f.long ?? "")).map((f) => f.attached);
|
|
2700
|
+
return [
|
|
2701
|
+
...args.filter(
|
|
2702
|
+
(a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
|
|
2703
|
+
).map((a) => a.value),
|
|
2704
|
+
...inline
|
|
2705
|
+
];
|
|
2706
|
+
}
|
|
2707
|
+
case "archive":
|
|
2708
|
+
src = archiveInputs(shape.archive, args, tail);
|
|
2709
|
+
break;
|
|
2710
|
+
case "allButLast":
|
|
2711
|
+
src = targetDir || dynamicDest ? args : args.slice(0, -1);
|
|
2712
|
+
break;
|
|
2713
|
+
}
|
|
2714
|
+
return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
|
|
2715
|
+
}
|
|
2716
|
+
function archiveInputs(kind, args, tail) {
|
|
2717
|
+
const first = args[0];
|
|
2718
|
+
const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
|
|
2719
|
+
if (kind === "tar") {
|
|
2720
|
+
const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
|
|
2721
|
+
const mode = (bareKey ? first.value : "") + flagsText;
|
|
2722
|
+
const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
|
|
2723
|
+
const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
|
|
2724
|
+
if (extracting && !writing) return [];
|
|
2725
|
+
void mode;
|
|
2726
|
+
let i = 0;
|
|
2727
|
+
if (bareKey) {
|
|
2728
|
+
i = 1;
|
|
2729
|
+
const next = args[1];
|
|
2730
|
+
if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
|
|
2731
|
+
}
|
|
2732
|
+
return args.slice(i);
|
|
2733
|
+
}
|
|
2734
|
+
if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
|
|
2735
|
+
if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
|
|
2736
|
+
return args.slice(2);
|
|
2737
|
+
}
|
|
2738
|
+
var COPY_RULE_OF = {
|
|
2739
|
+
"shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
|
|
2740
|
+
"shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
|
|
2741
|
+
"shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
|
|
2742
|
+
"shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
|
|
2743
|
+
};
|
|
2744
|
+
function copyVerdictOf(hit) {
|
|
2745
|
+
if (!hit) return null;
|
|
2746
|
+
const ruleName = COPY_RULE_OF[hit.ruleName];
|
|
2747
|
+
if (!ruleName) return null;
|
|
2748
|
+
return {
|
|
2749
|
+
ruleName,
|
|
2750
|
+
verdict: "review",
|
|
2751
|
+
reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
|
|
2752
|
+
path: hit.path
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
2562
2755
|
function matchSensitivePath2(p) {
|
|
2563
2756
|
for (const sp of SENSITIVE_PATH_RULES) {
|
|
2564
2757
|
if (sp.match(p))
|
|
@@ -2566,14 +2759,15 @@ function matchSensitivePath2(p) {
|
|
|
2566
2759
|
}
|
|
2567
2760
|
return null;
|
|
2568
2761
|
}
|
|
2569
|
-
|
|
2570
|
-
|
|
2762
|
+
function baseWord(w) {
|
|
2763
|
+
return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
|
|
2764
|
+
}
|
|
2765
|
+
var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
|
|
2766
|
+
var positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
|
|
2571
2767
|
function wrappedReadPaths(words, name) {
|
|
2572
2768
|
if (name === "find") {
|
|
2573
|
-
const k = words
|
|
2574
|
-
|
|
2575
|
-
const firstPredicate = words.findIndex((w, i) => i > 0 && w !== null && w.startsWith("-"));
|
|
2576
|
-
return positionalAfter(words, 1, firstPredicate > 0 ? firstPredicate : k);
|
|
2769
|
+
const { k, starts } = findStartPoints(words, 0);
|
|
2770
|
+
return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
|
|
2577
2771
|
}
|
|
2578
2772
|
if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
|
|
2579
2773
|
const h = unwrapCommandHead(words);
|
|
@@ -3434,6 +3628,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3434
3628
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
3435
3629
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
3436
3630
|
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
3631
|
+
let pendingAstReview;
|
|
3437
3632
|
if (bashCommand !== null) {
|
|
3438
3633
|
const pipeVerdict = pipeChainVerdict(
|
|
3439
3634
|
bashCommand,
|
|
@@ -3445,7 +3640,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3445
3640
|
if (fsVerdict) {
|
|
3446
3641
|
const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
|
|
3447
3642
|
const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
|
|
3448
|
-
|
|
3643
|
+
const astVerdict = {
|
|
3449
3644
|
decision: fsVerdict.verdict,
|
|
3450
3645
|
blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
|
|
3451
3646
|
reason: fsVerdict.reason,
|
|
@@ -3453,6 +3648,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3453
3648
|
ruleName: fsVerdict.ruleName,
|
|
3454
3649
|
ruleDescription: fsVerdict.reason
|
|
3455
3650
|
};
|
|
3651
|
+
if (fsVerdict.verdict === "block") return astVerdict;
|
|
3652
|
+
pendingAstReview = astVerdict;
|
|
3456
3653
|
}
|
|
3457
3654
|
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
3458
3655
|
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
@@ -3495,7 +3692,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3495
3692
|
const matchedRule = resolvePinned(matches);
|
|
3496
3693
|
if (matchedRule) {
|
|
3497
3694
|
if (matchedRule.verdict === "allow")
|
|
3498
|
-
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
3695
|
+
return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
3499
3696
|
return {
|
|
3500
3697
|
decision: matchedRule.verdict,
|
|
3501
3698
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -3522,6 +3719,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3522
3719
|
allTokens = analyzed.allTokens;
|
|
3523
3720
|
pathTokens = analyzed.paths;
|
|
3524
3721
|
const candidates = [];
|
|
3722
|
+
if (pendingAstReview) candidates.push(pendingAstReview);
|
|
3525
3723
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
3526
3724
|
if (evalVerdict === "block") {
|
|
3527
3725
|
return {
|