@node9/proxy 2.13.0 → 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/README.md +1 -1
- package/dist/cli.js +763 -521
- package/dist/cli.mjs +763 -521
- package/dist/dashboard.mjs +2806 -2331
- package/dist/index.js +375 -92
- package/dist/index.mjs +375 -92
- package/dist/scan-ink.mjs +86 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1162,13 +1162,32 @@ var DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
|
|
|
1162
1162
|
})
|
|
1163
1163
|
);
|
|
1164
1164
|
var SENSITIVE_PATH_PATTERNS = [
|
|
1165
|
-
/[/\\]\.ssh[/\\]/i,
|
|
1166
|
-
/[/\\]\.aws[/\\]/i,
|
|
1165
|
+
/[/\\]\.ssh([/\\]|$)/i,
|
|
1166
|
+
/[/\\]\.aws([/\\]|$)/i,
|
|
1167
1167
|
/[/\\]\.config[/\\]gcloud[/\\]/i,
|
|
1168
1168
|
/[/\\]\.azure[/\\]/i,
|
|
1169
1169
|
/[/\\]\.kube[/\\]config$/i,
|
|
1170
|
-
|
|
1171
|
-
// .
|
|
1170
|
+
// ⚠️ ONE SEMANTIC, FOUR COPIES. This is the AST tier's `.env` rule verbatim
|
|
1171
|
+
// (shell/index.ts SENSITIVE_PATH_RULES), whose reasoning is documented there:
|
|
1172
|
+
// structural suffix chain rather than a hand-written list, `example|sample|
|
|
1173
|
+
// template` exempt because a fixture stays a fixture whatever follows, and
|
|
1174
|
+
// `.test` anchored because `test` names an ENVIRONMENT -- `.env.test` is the
|
|
1175
|
+
// committed template, `.env.test.local` is gitignored and holds real values.
|
|
1176
|
+
//
|
|
1177
|
+
// It was previously `[/\\]\.env($|\.)` with NO exemptions, so `Read .env.example`
|
|
1178
|
+
// blocked while `cat .env.example` allowed: the same file, opposite verdicts,
|
|
1179
|
+
// decided only by which tool asked. See src/__tests__/jail-both-doors.test.ts,
|
|
1180
|
+
// which is the contract that now holds these copies in step, and stage 5 of
|
|
1181
|
+
// doc/credential-jail-architecture.md, which replaces them with one generated
|
|
1182
|
+
// source.
|
|
1183
|
+
// ⚠️ The `.local` branch comes FIRST and takes no exemption. A fixture stays a
|
|
1184
|
+
// fixture whatever follows it -- `.env.example.md` is documentation -- but
|
|
1185
|
+
// `.env.example.local` is gitignored by the `.env*.local` convention and holds
|
|
1186
|
+
// real values, exactly the reasoning that anchors `(?!\.test$)` rather than
|
|
1187
|
+
// using `\b`. Without this branch the fixture exemption also bought a two-step
|
|
1188
|
+
// bypass: `cp .env .env.sample`, then read the copy.
|
|
1189
|
+
/[/\\]\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i,
|
|
1190
|
+
// .env + any suffix chain; fixtures exempt unless .local
|
|
1172
1191
|
/[/\\]\.git-credentials$/i,
|
|
1173
1192
|
/[/\\]\.npmrc$/i,
|
|
1174
1193
|
/[/\\]\.docker[/\\]config\.json$/i,
|
|
@@ -1670,6 +1689,10 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1670
1689
|
"od",
|
|
1671
1690
|
"xxd",
|
|
1672
1691
|
"hexdump",
|
|
1692
|
+
// Emits the file's bytes, re-encoded, so it is a read by the set's own test
|
|
1693
|
+
// ("does it emit file contents"). Absent until 2026-09-10, which is why
|
|
1694
|
+
// `base64 ~/.ssh/id_rsa` printed a private key with no verdict.
|
|
1695
|
+
"base64",
|
|
1673
1696
|
"strings",
|
|
1674
1697
|
"sort",
|
|
1675
1698
|
"uniq",
|
|
@@ -1677,8 +1700,56 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1677
1700
|
"nl",
|
|
1678
1701
|
"dd"
|
|
1679
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]));
|
|
1680
1747
|
var FS_OP_PRESCREEN_RE = new RegExp(
|
|
1681
|
-
|
|
1748
|
+
// A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
|
|
1749
|
+
// reader right after `"` / `'`, and without these two characters the
|
|
1750
|
+
// prescreen rejected every string-wrapped read before the parser ran.
|
|
1751
|
+
// Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
|
|
1752
|
+
`(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
1682
1753
|
);
|
|
1683
1754
|
var HOME_CACHE_ALLOWLIST = [
|
|
1684
1755
|
".cache",
|
|
@@ -1699,12 +1770,12 @@ var SENSITIVE_PATH_RULES = [
|
|
|
1699
1770
|
{
|
|
1700
1771
|
rule: "shield:project-jail:block-read-ssh",
|
|
1701
1772
|
reason: "Reading SSH private keys is blocked by project-jail shield",
|
|
1702
|
-
match: (p) => /(
|
|
1773
|
+
match: (p) => /([\\/]\.ssh[\\/]|^\.ssh[\\/]|^(?:[~/]|[A-Za-z]:).*[\\/]\.ssh$)/i.test(p)
|
|
1703
1774
|
},
|
|
1704
1775
|
{
|
|
1705
1776
|
rule: "shield:project-jail:block-read-aws",
|
|
1706
1777
|
reason: "Reading AWS credentials is blocked by project-jail shield",
|
|
1707
|
-
match: (p) => /(
|
|
1778
|
+
match: (p) => /([\\/]\.aws[\\/]|^\.aws[\\/]|^(?:[~/]|[A-Za-z]:).*[\\/]\.aws$)/i.test(p)
|
|
1708
1779
|
},
|
|
1709
1780
|
{
|
|
1710
1781
|
// Mirrors the JSON shield's `.env` pattern (project-jail.json's
|
|
@@ -1748,7 +1819,9 @@ var SENSITIVE_PATH_RULES = [
|
|
|
1748
1819
|
// symmetry — silently exempts every `.env.test.*` file.
|
|
1749
1820
|
//
|
|
1750
1821
|
// shields.test.ts:983-995 is the canonical contract; keep both in step.
|
|
1751
|
-
match: (p) => /(?:^|[\\/])\.env(?![\w-])(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]
|
|
1822
|
+
match: (p) => /(?:^|[\\/])\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i.test(
|
|
1823
|
+
p
|
|
1824
|
+
)
|
|
1752
1825
|
},
|
|
1753
1826
|
{
|
|
1754
1827
|
// verdict: 'review' (not 'block') is a deliberate design choice
|
|
@@ -1959,15 +2032,14 @@ function listOps() {
|
|
|
1959
2032
|
return _listOps;
|
|
1960
2033
|
}
|
|
1961
2034
|
var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
|
|
2035
|
+
var FIND_EXEC_FLAGS = /* @__PURE__ */ new Set(["-exec", "-execdir", "-ok", "-okdir"]);
|
|
1962
2036
|
var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
1963
2037
|
function unwrapCommandHead(words) {
|
|
1964
2038
|
let i = 0;
|
|
1965
2039
|
while (i < words.length) {
|
|
1966
2040
|
const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
|
|
1967
2041
|
if (head === "find") {
|
|
1968
|
-
const x = words.findIndex(
|
|
1969
|
-
(w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
|
|
1970
|
-
);
|
|
2042
|
+
const x = words.findIndex((w, k) => k > i && w !== null && FIND_EXEC_FLAGS.has(w));
|
|
1971
2043
|
if (x < 0) break;
|
|
1972
2044
|
i = x + 1;
|
|
1973
2045
|
continue;
|
|
@@ -1989,7 +2061,11 @@ function unwrapCommandHead(words) {
|
|
|
1989
2061
|
if (t.startsWith("-")) {
|
|
1990
2062
|
i++;
|
|
1991
2063
|
const nxt = words[i];
|
|
1992
|
-
if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase())
|
|
2064
|
+
if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase()) && // A reader is a command, never a flag's operand: `env - cat X`,
|
|
2065
|
+
// `stdbuf -o0 cat X`, `ionice -c3 cat X`. Without this the head was
|
|
2066
|
+
// swallowed and the jail needed a looser fallback whose cost was a
|
|
2067
|
+
// false positive on `sudo echo cat X`.
|
|
2068
|
+
!FS_READ_TOOLS.has(nxt.split("/").pop()?.toLowerCase() ?? ""))
|
|
1993
2069
|
i++;
|
|
1994
2070
|
continue;
|
|
1995
2071
|
}
|
|
@@ -2108,38 +2184,43 @@ function isProtectedHomePath(rawPath) {
|
|
|
2108
2184
|
}
|
|
2109
2185
|
return true;
|
|
2110
2186
|
}
|
|
2111
|
-
function
|
|
2112
|
-
const
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
const
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
|
|
2120
|
-
else if (t === "SglQuoted") s += p.Value ?? "";
|
|
2121
|
-
else if (t === "DblQuoted") {
|
|
2122
|
-
const inner = p.Parts || [];
|
|
2123
|
-
if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
|
|
2124
|
-
s += inner.map((ip) => ip.Value ?? "").join("");
|
|
2125
|
-
} else {
|
|
2126
|
-
return null;
|
|
2127
|
-
}
|
|
2187
|
+
function positionedArgs(words, from = 1, to = words.length) {
|
|
2188
|
+
const out = [];
|
|
2189
|
+
let afterFlag = null;
|
|
2190
|
+
for (let i = from; i < to; i++) {
|
|
2191
|
+
const v = words[i];
|
|
2192
|
+
if (v === null) {
|
|
2193
|
+
afterFlag = null;
|
|
2194
|
+
continue;
|
|
2128
2195
|
}
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
const v = litFromWord(args[i]);
|
|
2136
|
-
if (v === null) continue;
|
|
2137
|
-
if (v.startsWith("-")) flags.push(v);
|
|
2138
|
-
else paths.push(v);
|
|
2196
|
+
if (v.startsWith("-")) {
|
|
2197
|
+
afterFlag = v;
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
out.push({ value: v, index: out.length, argv: i, afterFlag });
|
|
2201
|
+
afterFlag = null;
|
|
2139
2202
|
}
|
|
2140
|
-
return
|
|
2203
|
+
return out;
|
|
2141
2204
|
}
|
|
2142
|
-
|
|
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 };
|
|
2213
|
+
}
|
|
2214
|
+
var NET_BINARIES = /* @__PURE__ */ new Set([
|
|
2215
|
+
"curl",
|
|
2216
|
+
"wget",
|
|
2217
|
+
"scp",
|
|
2218
|
+
"ssh",
|
|
2219
|
+
"nc",
|
|
2220
|
+
"ncat",
|
|
2221
|
+
"netcat",
|
|
2222
|
+
"rsync"
|
|
2223
|
+
]);
|
|
2143
2224
|
var VALUE_FLAGS = {
|
|
2144
2225
|
curl: /* @__PURE__ */ new Set([
|
|
2145
2226
|
"-d",
|
|
@@ -2424,6 +2505,9 @@ function deriveRedirOp(sample) {
|
|
|
2424
2505
|
}
|
|
2425
2506
|
}
|
|
2426
2507
|
var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
|
|
2508
|
+
var REDIR_FILE_IN_OPS = new Set(
|
|
2509
|
+
[deriveRedirOp("cat < f"), deriveRedirOp("cat <> f")].filter((op) => op >= 0)
|
|
2510
|
+
);
|
|
2427
2511
|
var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
|
|
2428
2512
|
deriveRedirOp("cat <<X\nX"),
|
|
2429
2513
|
deriveRedirOp("cat <<-X\nX")
|
|
@@ -2488,16 +2572,21 @@ function isRmCreatedInCommandCleanup(command) {
|
|
|
2488
2572
|
}
|
|
2489
2573
|
return sawRm && ok;
|
|
2490
2574
|
}
|
|
2491
|
-
function analyzeFsOperationImpl(command) {
|
|
2575
|
+
function analyzeFsOperationImpl(command, depth = 0) {
|
|
2492
2576
|
const f = parseShared(command);
|
|
2493
2577
|
if (f === PARSE_FAIL) return null;
|
|
2494
2578
|
let result = null;
|
|
2495
2579
|
try {
|
|
2496
2580
|
syntax.Walk(f, (node) => {
|
|
2497
|
-
if (!node || result) return false;
|
|
2581
|
+
if (!node || result?.verdict === "block") return false;
|
|
2498
2582
|
const n = node;
|
|
2499
|
-
|
|
2500
|
-
|
|
2583
|
+
const nodeType = syntax.NodeType(n);
|
|
2584
|
+
if (nodeType === "Stmt") {
|
|
2585
|
+
result = stricter(result, jailedRedirectRead(n));
|
|
2586
|
+
return result?.verdict !== "block";
|
|
2587
|
+
}
|
|
2588
|
+
if (nodeType !== "CallExpr") return true;
|
|
2589
|
+
const { name, flags, paths, words } = extractLiteralArgs(n);
|
|
2501
2590
|
if (!name) return true;
|
|
2502
2591
|
if (name === "rm") {
|
|
2503
2592
|
const flagStr = flags.join("").toLowerCase();
|
|
@@ -2526,21 +2615,27 @@ function analyzeFsOperationImpl(command) {
|
|
|
2526
2615
|
}
|
|
2527
2616
|
}
|
|
2528
2617
|
}
|
|
2529
|
-
if (
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
reason: sp.reason,
|
|
2537
|
-
path: p
|
|
2538
|
-
};
|
|
2539
|
-
return false;
|
|
2540
|
-
}
|
|
2618
|
+
if (depth < 1) {
|
|
2619
|
+
const payload = literalShellPayload(words, name);
|
|
2620
|
+
if (payload !== null) {
|
|
2621
|
+
const inner = analyzeFsOperationImpl(payload, depth + 1);
|
|
2622
|
+
if (inner) {
|
|
2623
|
+
result = inner;
|
|
2624
|
+
return false;
|
|
2541
2625
|
}
|
|
2626
|
+
return true;
|
|
2542
2627
|
}
|
|
2543
2628
|
}
|
|
2629
|
+
const readPaths = FS_READ_TOOLS.has(name) ? paths : wrappedReadPaths(words, name);
|
|
2630
|
+
if (readPaths) {
|
|
2631
|
+
for (const p of readPaths) {
|
|
2632
|
+
result = stricter(result, matchSensitivePath2(p));
|
|
2633
|
+
if (result?.verdict === "block") return false;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
for (const p of copySourcePaths(words)) {
|
|
2637
|
+
result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
|
|
2638
|
+
}
|
|
2544
2639
|
return true;
|
|
2545
2640
|
});
|
|
2546
2641
|
return result;
|
|
@@ -2548,6 +2643,192 @@ function analyzeFsOperationImpl(command) {
|
|
|
2548
2643
|
return null;
|
|
2549
2644
|
}
|
|
2550
2645
|
}
|
|
2646
|
+
function stricter(a, b) {
|
|
2647
|
+
if (!a) return b;
|
|
2648
|
+
if (!b) return a;
|
|
2649
|
+
return b.verdict === "block" && a.verdict !== "block" ? b : a;
|
|
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
|
+
}
|
|
2785
|
+
function matchSensitivePath2(p) {
|
|
2786
|
+
for (const sp of SENSITIVE_PATH_RULES) {
|
|
2787
|
+
if (sp.match(p))
|
|
2788
|
+
return { ruleName: sp.rule, verdict: sp.verdict ?? "block", reason: sp.reason, path: p };
|
|
2789
|
+
}
|
|
2790
|
+
return null;
|
|
2791
|
+
}
|
|
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);
|
|
2797
|
+
function wrappedReadPaths(words, name) {
|
|
2798
|
+
if (name === "find") {
|
|
2799
|
+
const { k, starts } = findStartPoints(words, 0);
|
|
2800
|
+
return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
|
|
2801
|
+
}
|
|
2802
|
+
if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
|
|
2803
|
+
const h = unwrapCommandHead(words);
|
|
2804
|
+
return h > 0 && isReaderWord(words[h] ?? null) ? positionalAfter(words, h + 1) : null;
|
|
2805
|
+
}
|
|
2806
|
+
function literalShellPayload(words, name) {
|
|
2807
|
+
const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
|
|
2808
|
+
const head = (words[h] ?? "").split("/").pop()?.toLowerCase() ?? "";
|
|
2809
|
+
if (head === "eval") {
|
|
2810
|
+
const rest = words.slice(h + 1);
|
|
2811
|
+
if (rest.length === 0 || rest.some((w) => w === null)) return null;
|
|
2812
|
+
return rest.join(" ");
|
|
2813
|
+
}
|
|
2814
|
+
if (SHELL_INTERPRETERS.has(head)) {
|
|
2815
|
+
const c = words.findIndex((w, i) => i > h && w !== null && isInlineCodeFlag(head, w));
|
|
2816
|
+
if (c < 0) return null;
|
|
2817
|
+
return words[c + 1] ?? null;
|
|
2818
|
+
}
|
|
2819
|
+
return null;
|
|
2820
|
+
}
|
|
2821
|
+
function jailedRedirectRead(stmt) {
|
|
2822
|
+
const redirs = stmt.Redirs || [];
|
|
2823
|
+
for (const r of redirs) {
|
|
2824
|
+
if (!r || !REDIR_FILE_IN_OPS.has(r.Op)) continue;
|
|
2825
|
+
const p = resolveWordLiteral(r.Word);
|
|
2826
|
+
if (p === null || p === "") continue;
|
|
2827
|
+
const hit = matchSensitivePath2(p);
|
|
2828
|
+
if (hit) return hit;
|
|
2829
|
+
}
|
|
2830
|
+
return null;
|
|
2831
|
+
}
|
|
2551
2832
|
function analyzeShellCommand(command) {
|
|
2552
2833
|
const actions = [];
|
|
2553
2834
|
const paths = [];
|
|
@@ -2680,21 +2961,7 @@ function evaluateEgress(dests, policy) {
|
|
|
2680
2961
|
}
|
|
2681
2962
|
return review;
|
|
2682
2963
|
}
|
|
2683
|
-
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
2684
|
-
"cat",
|
|
2685
|
-
"head",
|
|
2686
|
-
"tail",
|
|
2687
|
-
"grep",
|
|
2688
|
-
"awk",
|
|
2689
|
-
"sed",
|
|
2690
|
-
"cut",
|
|
2691
|
-
"sort",
|
|
2692
|
-
"tee",
|
|
2693
|
-
"less",
|
|
2694
|
-
"more",
|
|
2695
|
-
"strings",
|
|
2696
|
-
"xxd"
|
|
2697
|
-
]);
|
|
2964
|
+
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
|
|
2698
2965
|
var SINK_COMMANDS = /* @__PURE__ */ new Set([
|
|
2699
2966
|
"curl",
|
|
2700
2967
|
"wget",
|
|
@@ -2725,16 +2992,25 @@ var OBFUSCATORS = /* @__PURE__ */ new Set([
|
|
|
2725
2992
|
"node"
|
|
2726
2993
|
]);
|
|
2727
2994
|
var SENSITIVE_PATTERNS = [
|
|
2728
|
-
/
|
|
2729
|
-
|
|
2995
|
+
// Kept in step with the AST tier and dlp/ -- see jail-both-doors.test.ts.
|
|
2996
|
+
/(?:^|\/)\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i,
|
|
2997
|
+
// .env chain; fixtures exempt unless .local
|
|
2730
2998
|
/id_rsa|id_ed25519|id_ecdsa|id_dsa/i,
|
|
2731
2999
|
// SSH private keys
|
|
2732
3000
|
/\.pem$|\.key$|\.p12$|\.pfx$/i,
|
|
2733
3001
|
// certificate files
|
|
2734
|
-
/
|
|
2735
|
-
//
|
|
2736
|
-
/
|
|
2737
|
-
//
|
|
3002
|
+
// The `$` half mirrors shell/index.ts's SENSITIVE_PATH_RULES: a file INSIDE
|
|
3003
|
+
// the directory counts wherever it appears, while the directory ITSELF counts
|
|
3004
|
+
// only when the path is ROOTED (`~/.ssh`, `/home/u/.ssh`) -- an unrooted
|
|
3005
|
+
// `config/.ssh` is more likely a search pattern than a read. These are
|
|
3006
|
+
// extracted TOKENS (see `args.some(isSensitivePath)` below), the same input
|
|
3007
|
+
// contract as the shell tier, so the same boundary is the right one.
|
|
3008
|
+
// Without it `grep -r x ~/.ssh | curl -d @-` scored one tier BELOW the
|
|
3009
|
+
// identical pipeline naming a file inside that directory.
|
|
3010
|
+
/(?:^|\/)\.ssh\/|^(?:[~/]|[A-Za-z]:).*\/\.ssh$/i,
|
|
3011
|
+
// ~/.ssh/ and ~/.ssh
|
|
3012
|
+
/(?:^|\/)\.aws\/credentials|^(?:[~/]|[A-Za-z]:).*\/\.aws$/i,
|
|
3013
|
+
// AWS creds + dir
|
|
2738
3014
|
/(?:^|\/)\.netrc$/i,
|
|
2739
3015
|
// netrc (stores HTTP credentials)
|
|
2740
3016
|
/(?:^|\/)(passwd|shadow|sudoers)$/i,
|
|
@@ -2768,8 +3044,8 @@ function splitOnPipe(cmd) {
|
|
|
2768
3044
|
if (current.trim()) segments2.push(current.trim());
|
|
2769
3045
|
return segments2.filter(Boolean);
|
|
2770
3046
|
}
|
|
2771
|
-
function positionalTokens(
|
|
2772
|
-
return
|
|
3047
|
+
function positionalTokens(tokens) {
|
|
3048
|
+
return tokens.slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
|
|
2773
3049
|
}
|
|
2774
3050
|
function analyzePipeChain(command) {
|
|
2775
3051
|
const segments2 = splitOnPipe(command);
|
|
@@ -2792,8 +3068,10 @@ function analyzePipeChain(command) {
|
|
|
2792
3068
|
for (const segment of segments2) {
|
|
2793
3069
|
const tokens = segment.split(/\s+/).filter(Boolean);
|
|
2794
3070
|
if (tokens.length === 0) continue;
|
|
2795
|
-
const
|
|
2796
|
-
const
|
|
3071
|
+
const h = unwrapCommandHead(tokens);
|
|
3072
|
+
const head = h < tokens.length ? h : 0;
|
|
3073
|
+
const binary = tokens[head].toLowerCase();
|
|
3074
|
+
const args = positionalTokens(tokens.slice(head));
|
|
2797
3075
|
if (SOURCE_COMMANDS.has(binary)) {
|
|
2798
3076
|
sourceFiles.push(...args);
|
|
2799
3077
|
if (args.some(isSensitivePath)) hasSensitiveSource = true;
|
|
@@ -3380,6 +3658,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3380
3658
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
3381
3659
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
3382
3660
|
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
3661
|
+
let pendingAstReview;
|
|
3383
3662
|
if (bashCommand !== null) {
|
|
3384
3663
|
const pipeVerdict = pipeChainVerdict(
|
|
3385
3664
|
bashCommand,
|
|
@@ -3391,7 +3670,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3391
3670
|
if (fsVerdict) {
|
|
3392
3671
|
const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
|
|
3393
3672
|
const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
|
|
3394
|
-
|
|
3673
|
+
const astVerdict = {
|
|
3395
3674
|
decision: fsVerdict.verdict,
|
|
3396
3675
|
blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
|
|
3397
3676
|
reason: fsVerdict.reason,
|
|
@@ -3399,6 +3678,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3399
3678
|
ruleName: fsVerdict.ruleName,
|
|
3400
3679
|
ruleDescription: fsVerdict.reason
|
|
3401
3680
|
};
|
|
3681
|
+
if (fsVerdict.verdict === "block") return astVerdict;
|
|
3682
|
+
pendingAstReview = astVerdict;
|
|
3402
3683
|
}
|
|
3403
3684
|
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
3404
3685
|
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
@@ -3441,7 +3722,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3441
3722
|
const matchedRule = resolvePinned(matches);
|
|
3442
3723
|
if (matchedRule) {
|
|
3443
3724
|
if (matchedRule.verdict === "allow")
|
|
3444
|
-
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
3725
|
+
return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
3445
3726
|
return {
|
|
3446
3727
|
decision: matchedRule.verdict,
|
|
3447
3728
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -3468,6 +3749,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3468
3749
|
allTokens = analyzed.allTokens;
|
|
3469
3750
|
pathTokens = analyzed.paths;
|
|
3470
3751
|
const candidates = [];
|
|
3752
|
+
if (pendingAstReview) candidates.push(pendingAstReview);
|
|
3471
3753
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
3472
3754
|
if (evalVerdict === "block") {
|
|
3473
3755
|
return {
|
|
@@ -4192,7 +4474,7 @@ var project_jail_default = {
|
|
|
4192
4474
|
{
|
|
4193
4475
|
field: "command",
|
|
4194
4476
|
op: "matches",
|
|
4195
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
|
|
4477
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.ssh[\\/\\\\]",
|
|
4196
4478
|
flags: "i"
|
|
4197
4479
|
}
|
|
4198
4480
|
],
|
|
@@ -4206,7 +4488,7 @@ var project_jail_default = {
|
|
|
4206
4488
|
{
|
|
4207
4489
|
field: "command",
|
|
4208
4490
|
op: "matches",
|
|
4209
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
|
|
4491
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.aws[\\/\\\\]",
|
|
4210
4492
|
flags: "i"
|
|
4211
4493
|
}
|
|
4212
4494
|
],
|
|
@@ -4220,7 +4502,7 @@ var project_jail_default = {
|
|
|
4220
4502
|
{
|
|
4221
4503
|
field: "command",
|
|
4222
4504
|
op: "matches",
|
|
4223
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
|
|
4505
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*?\\.env(\\.(local|production|staging|development|production\\.local|staging\\.local|development\\.local))?(?=\\s|$|[;&|>)<])",
|
|
4224
4506
|
flags: "i"
|
|
4225
4507
|
}
|
|
4226
4508
|
],
|
|
@@ -4234,7 +4516,7 @@ var project_jail_default = {
|
|
|
4234
4516
|
{
|
|
4235
4517
|
field: "command",
|
|
4236
4518
|
op: "matches",
|
|
4237
|
-
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
|
|
4519
|
+
value: "(cat|less|head|tail|bat|more|open|print|nano|vim|vi|emacs|code|type|grep|egrep|fgrep|rg|ag|ack|awk|gawk|sed|cut|tr|jq|yq|od|xxd|hexdump|strings|base64|sort|uniq|tac|nl|dd)\\s+.*(credentials\\.json|\\.netrc|\\.npmrc|\\.docker[\\/\\\\]config\\.json|gcloud[\\/\\\\]credentials)",
|
|
4238
4520
|
flags: "i"
|
|
4239
4521
|
}
|
|
4240
4522
|
],
|
|
@@ -4248,7 +4530,7 @@ var project_jail_default = {
|
|
|
4248
4530
|
{
|
|
4249
4531
|
field: "file_path",
|
|
4250
4532
|
op: "matches",
|
|
4251
|
-
value: "(
|
|
4533
|
+
value: "([\\/\\\\]\\.ssh([\\/\\\\]|$)|^\\.ssh[\\/\\\\])",
|
|
4252
4534
|
flags: "i"
|
|
4253
4535
|
}
|
|
4254
4536
|
],
|
|
@@ -4262,7 +4544,7 @@ var project_jail_default = {
|
|
|
4262
4544
|
{
|
|
4263
4545
|
field: "file_path",
|
|
4264
4546
|
op: "matches",
|
|
4265
|
-
value: "(
|
|
4547
|
+
value: "([\\/\\\\]\\.aws([\\/\\\\]|$)|^\\.aws[\\/\\\\])",
|
|
4266
4548
|
flags: "i"
|
|
4267
4549
|
}
|
|
4268
4550
|
],
|
|
@@ -4276,7 +4558,7 @@ var project_jail_default = {
|
|
|
4276
4558
|
{
|
|
4277
4559
|
field: "file_path",
|
|
4278
4560
|
op: "matches",
|
|
4279
|
-
value: "(^|[\\/\\\\])\\.env(
|
|
4561
|
+
value: "(^|[\\/\\\\])\\.env(?![\\w-])(?:[\\w.-]*\\.local$|(?!\\.(example|sample|template)\\b)(?!\\.test$)[\\w.-]*$)",
|
|
4280
4562
|
flags: "i"
|
|
4281
4563
|
}
|
|
4282
4564
|
],
|
|
@@ -7053,12 +7335,13 @@ function extractFilePaths(toolName, args) {
|
|
|
7053
7335
|
}
|
|
7054
7336
|
return paths.filter(Boolean);
|
|
7055
7337
|
}
|
|
7338
|
+
var NETWORK_COMMAND_RE = new RegExp(`(?<![.\\w-])(${[...NET_BINARIES].join("|")})\\b`);
|
|
7056
7339
|
function isNetworkTool(toolName, args) {
|
|
7057
7340
|
const t = toolName.toLowerCase();
|
|
7058
7341
|
if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal.execute") {
|
|
7059
7342
|
const a = args;
|
|
7060
7343
|
const cmd = typeof a?.command === "string" ? a.command : typeof a?.cmd === "string" ? a.cmd : "";
|
|
7061
|
-
return
|
|
7344
|
+
return NETWORK_COMMAND_RE.test(cmd);
|
|
7062
7345
|
}
|
|
7063
7346
|
return false;
|
|
7064
7347
|
}
|