@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.mjs
CHANGED
|
@@ -1132,13 +1132,32 @@ var DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
|
|
|
1132
1132
|
})
|
|
1133
1133
|
);
|
|
1134
1134
|
var SENSITIVE_PATH_PATTERNS = [
|
|
1135
|
-
/[/\\]\.ssh[/\\]/i,
|
|
1136
|
-
/[/\\]\.aws[/\\]/i,
|
|
1135
|
+
/[/\\]\.ssh([/\\]|$)/i,
|
|
1136
|
+
/[/\\]\.aws([/\\]|$)/i,
|
|
1137
1137
|
/[/\\]\.config[/\\]gcloud[/\\]/i,
|
|
1138
1138
|
/[/\\]\.azure[/\\]/i,
|
|
1139
1139
|
/[/\\]\.kube[/\\]config$/i,
|
|
1140
|
-
|
|
1141
|
-
// .
|
|
1140
|
+
// ⚠️ ONE SEMANTIC, FOUR COPIES. This is the AST tier's `.env` rule verbatim
|
|
1141
|
+
// (shell/index.ts SENSITIVE_PATH_RULES), whose reasoning is documented there:
|
|
1142
|
+
// structural suffix chain rather than a hand-written list, `example|sample|
|
|
1143
|
+
// template` exempt because a fixture stays a fixture whatever follows, and
|
|
1144
|
+
// `.test` anchored because `test` names an ENVIRONMENT -- `.env.test` is the
|
|
1145
|
+
// committed template, `.env.test.local` is gitignored and holds real values.
|
|
1146
|
+
//
|
|
1147
|
+
// It was previously `[/\\]\.env($|\.)` with NO exemptions, so `Read .env.example`
|
|
1148
|
+
// blocked while `cat .env.example` allowed: the same file, opposite verdicts,
|
|
1149
|
+
// decided only by which tool asked. See src/__tests__/jail-both-doors.test.ts,
|
|
1150
|
+
// which is the contract that now holds these copies in step, and stage 5 of
|
|
1151
|
+
// doc/credential-jail-architecture.md, which replaces them with one generated
|
|
1152
|
+
// source.
|
|
1153
|
+
// ⚠️ The `.local` branch comes FIRST and takes no exemption. A fixture stays a
|
|
1154
|
+
// fixture whatever follows it -- `.env.example.md` is documentation -- but
|
|
1155
|
+
// `.env.example.local` is gitignored by the `.env*.local` convention and holds
|
|
1156
|
+
// real values, exactly the reasoning that anchors `(?!\.test$)` rather than
|
|
1157
|
+
// using `\b`. Without this branch the fixture exemption also bought a two-step
|
|
1158
|
+
// bypass: `cp .env .env.sample`, then read the copy.
|
|
1159
|
+
/[/\\]\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i,
|
|
1160
|
+
// .env + any suffix chain; fixtures exempt unless .local
|
|
1142
1161
|
/[/\\]\.git-credentials$/i,
|
|
1143
1162
|
/[/\\]\.npmrc$/i,
|
|
1144
1163
|
/[/\\]\.docker[/\\]config\.json$/i,
|
|
@@ -1640,6 +1659,10 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1640
1659
|
"od",
|
|
1641
1660
|
"xxd",
|
|
1642
1661
|
"hexdump",
|
|
1662
|
+
// Emits the file's bytes, re-encoded, so it is a read by the set's own test
|
|
1663
|
+
// ("does it emit file contents"). Absent until 2026-09-10, which is why
|
|
1664
|
+
// `base64 ~/.ssh/id_rsa` printed a private key with no verdict.
|
|
1665
|
+
"base64",
|
|
1643
1666
|
"strings",
|
|
1644
1667
|
"sort",
|
|
1645
1668
|
"uniq",
|
|
@@ -1647,8 +1670,56 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1647
1670
|
"nl",
|
|
1648
1671
|
"dd"
|
|
1649
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]));
|
|
1650
1717
|
var FS_OP_PRESCREEN_RE = new RegExp(
|
|
1651
|
-
|
|
1718
|
+
// A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
|
|
1719
|
+
// reader right after `"` / `'`, and without these two characters the
|
|
1720
|
+
// prescreen rejected every string-wrapped read before the parser ran.
|
|
1721
|
+
// Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
|
|
1722
|
+
`(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
1652
1723
|
);
|
|
1653
1724
|
var HOME_CACHE_ALLOWLIST = [
|
|
1654
1725
|
".cache",
|
|
@@ -1669,12 +1740,12 @@ var SENSITIVE_PATH_RULES = [
|
|
|
1669
1740
|
{
|
|
1670
1741
|
rule: "shield:project-jail:block-read-ssh",
|
|
1671
1742
|
reason: "Reading SSH private keys is blocked by project-jail shield",
|
|
1672
|
-
match: (p) => /(
|
|
1743
|
+
match: (p) => /([\\/]\.ssh[\\/]|^\.ssh[\\/]|^(?:[~/]|[A-Za-z]:).*[\\/]\.ssh$)/i.test(p)
|
|
1673
1744
|
},
|
|
1674
1745
|
{
|
|
1675
1746
|
rule: "shield:project-jail:block-read-aws",
|
|
1676
1747
|
reason: "Reading AWS credentials is blocked by project-jail shield",
|
|
1677
|
-
match: (p) => /(
|
|
1748
|
+
match: (p) => /([\\/]\.aws[\\/]|^\.aws[\\/]|^(?:[~/]|[A-Za-z]:).*[\\/]\.aws$)/i.test(p)
|
|
1678
1749
|
},
|
|
1679
1750
|
{
|
|
1680
1751
|
// Mirrors the JSON shield's `.env` pattern (project-jail.json's
|
|
@@ -1718,7 +1789,9 @@ var SENSITIVE_PATH_RULES = [
|
|
|
1718
1789
|
// symmetry — silently exempts every `.env.test.*` file.
|
|
1719
1790
|
//
|
|
1720
1791
|
// shields.test.ts:983-995 is the canonical contract; keep both in step.
|
|
1721
|
-
match: (p) => /(?:^|[\\/])\.env(?![\w-])(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]
|
|
1792
|
+
match: (p) => /(?:^|[\\/])\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i.test(
|
|
1793
|
+
p
|
|
1794
|
+
)
|
|
1722
1795
|
},
|
|
1723
1796
|
{
|
|
1724
1797
|
// verdict: 'review' (not 'block') is a deliberate design choice
|
|
@@ -1929,15 +2002,14 @@ function listOps() {
|
|
|
1929
2002
|
return _listOps;
|
|
1930
2003
|
}
|
|
1931
2004
|
var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
|
|
2005
|
+
var FIND_EXEC_FLAGS = /* @__PURE__ */ new Set(["-exec", "-execdir", "-ok", "-okdir"]);
|
|
1932
2006
|
var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
1933
2007
|
function unwrapCommandHead(words) {
|
|
1934
2008
|
let i = 0;
|
|
1935
2009
|
while (i < words.length) {
|
|
1936
2010
|
const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
|
|
1937
2011
|
if (head === "find") {
|
|
1938
|
-
const x = words.findIndex(
|
|
1939
|
-
(w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
|
|
1940
|
-
);
|
|
2012
|
+
const x = words.findIndex((w, k) => k > i && w !== null && FIND_EXEC_FLAGS.has(w));
|
|
1941
2013
|
if (x < 0) break;
|
|
1942
2014
|
i = x + 1;
|
|
1943
2015
|
continue;
|
|
@@ -1959,7 +2031,11 @@ function unwrapCommandHead(words) {
|
|
|
1959
2031
|
if (t.startsWith("-")) {
|
|
1960
2032
|
i++;
|
|
1961
2033
|
const nxt = words[i];
|
|
1962
|
-
if (nxt != null && !nxt.startsWith("-") && !INLINE_INTERPRETER.test(nxt.split("/").pop() ?? "") && !COMMAND_WRAPPERS.has(nxt.toLowerCase()) && !RUNNER_WRAPPERS.has(nxt.toLowerCase())
|
|
2034
|
+
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`,
|
|
2035
|
+
// `stdbuf -o0 cat X`, `ionice -c3 cat X`. Without this the head was
|
|
2036
|
+
// swallowed and the jail needed a looser fallback whose cost was a
|
|
2037
|
+
// false positive on `sudo echo cat X`.
|
|
2038
|
+
!FS_READ_TOOLS.has(nxt.split("/").pop()?.toLowerCase() ?? ""))
|
|
1963
2039
|
i++;
|
|
1964
2040
|
continue;
|
|
1965
2041
|
}
|
|
@@ -2078,38 +2154,43 @@ function isProtectedHomePath(rawPath) {
|
|
|
2078
2154
|
}
|
|
2079
2155
|
return true;
|
|
2080
2156
|
}
|
|
2081
|
-
function
|
|
2082
|
-
const
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
const
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
|
|
2090
|
-
else if (t === "SglQuoted") s += p.Value ?? "";
|
|
2091
|
-
else if (t === "DblQuoted") {
|
|
2092
|
-
const inner = p.Parts || [];
|
|
2093
|
-
if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
|
|
2094
|
-
s += inner.map((ip) => ip.Value ?? "").join("");
|
|
2095
|
-
} else {
|
|
2096
|
-
return null;
|
|
2097
|
-
}
|
|
2157
|
+
function positionedArgs(words, from = 1, to = words.length) {
|
|
2158
|
+
const out = [];
|
|
2159
|
+
let afterFlag = null;
|
|
2160
|
+
for (let i = from; i < to; i++) {
|
|
2161
|
+
const v = words[i];
|
|
2162
|
+
if (v === null) {
|
|
2163
|
+
afterFlag = null;
|
|
2164
|
+
continue;
|
|
2098
2165
|
}
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
const v = litFromWord(args[i]);
|
|
2106
|
-
if (v === null) continue;
|
|
2107
|
-
if (v.startsWith("-")) flags.push(v);
|
|
2108
|
-
else paths.push(v);
|
|
2166
|
+
if (v.startsWith("-")) {
|
|
2167
|
+
afterFlag = v;
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
out.push({ value: v, index: out.length, argv: i, afterFlag });
|
|
2171
|
+
afterFlag = null;
|
|
2109
2172
|
}
|
|
2110
|
-
return
|
|
2173
|
+
return out;
|
|
2111
2174
|
}
|
|
2112
|
-
|
|
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 };
|
|
2183
|
+
}
|
|
2184
|
+
var NET_BINARIES = /* @__PURE__ */ new Set([
|
|
2185
|
+
"curl",
|
|
2186
|
+
"wget",
|
|
2187
|
+
"scp",
|
|
2188
|
+
"ssh",
|
|
2189
|
+
"nc",
|
|
2190
|
+
"ncat",
|
|
2191
|
+
"netcat",
|
|
2192
|
+
"rsync"
|
|
2193
|
+
]);
|
|
2113
2194
|
var VALUE_FLAGS = {
|
|
2114
2195
|
curl: /* @__PURE__ */ new Set([
|
|
2115
2196
|
"-d",
|
|
@@ -2394,6 +2475,9 @@ function deriveRedirOp(sample) {
|
|
|
2394
2475
|
}
|
|
2395
2476
|
}
|
|
2396
2477
|
var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
|
|
2478
|
+
var REDIR_FILE_IN_OPS = new Set(
|
|
2479
|
+
[deriveRedirOp("cat < f"), deriveRedirOp("cat <> f")].filter((op) => op >= 0)
|
|
2480
|
+
);
|
|
2397
2481
|
var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
|
|
2398
2482
|
deriveRedirOp("cat <<X\nX"),
|
|
2399
2483
|
deriveRedirOp("cat <<-X\nX")
|
|
@@ -2458,16 +2542,21 @@ function isRmCreatedInCommandCleanup(command) {
|
|
|
2458
2542
|
}
|
|
2459
2543
|
return sawRm && ok;
|
|
2460
2544
|
}
|
|
2461
|
-
function analyzeFsOperationImpl(command) {
|
|
2545
|
+
function analyzeFsOperationImpl(command, depth = 0) {
|
|
2462
2546
|
const f = parseShared(command);
|
|
2463
2547
|
if (f === PARSE_FAIL) return null;
|
|
2464
2548
|
let result = null;
|
|
2465
2549
|
try {
|
|
2466
2550
|
syntax.Walk(f, (node) => {
|
|
2467
|
-
if (!node || result) return false;
|
|
2551
|
+
if (!node || result?.verdict === "block") return false;
|
|
2468
2552
|
const n = node;
|
|
2469
|
-
|
|
2470
|
-
|
|
2553
|
+
const nodeType = syntax.NodeType(n);
|
|
2554
|
+
if (nodeType === "Stmt") {
|
|
2555
|
+
result = stricter(result, jailedRedirectRead(n));
|
|
2556
|
+
return result?.verdict !== "block";
|
|
2557
|
+
}
|
|
2558
|
+
if (nodeType !== "CallExpr") return true;
|
|
2559
|
+
const { name, flags, paths, words } = extractLiteralArgs(n);
|
|
2471
2560
|
if (!name) return true;
|
|
2472
2561
|
if (name === "rm") {
|
|
2473
2562
|
const flagStr = flags.join("").toLowerCase();
|
|
@@ -2496,21 +2585,27 @@ function analyzeFsOperationImpl(command) {
|
|
|
2496
2585
|
}
|
|
2497
2586
|
}
|
|
2498
2587
|
}
|
|
2499
|
-
if (
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
reason: sp.reason,
|
|
2507
|
-
path: p
|
|
2508
|
-
};
|
|
2509
|
-
return false;
|
|
2510
|
-
}
|
|
2588
|
+
if (depth < 1) {
|
|
2589
|
+
const payload = literalShellPayload(words, name);
|
|
2590
|
+
if (payload !== null) {
|
|
2591
|
+
const inner = analyzeFsOperationImpl(payload, depth + 1);
|
|
2592
|
+
if (inner) {
|
|
2593
|
+
result = inner;
|
|
2594
|
+
return false;
|
|
2511
2595
|
}
|
|
2596
|
+
return true;
|
|
2512
2597
|
}
|
|
2513
2598
|
}
|
|
2599
|
+
const readPaths = FS_READ_TOOLS.has(name) ? paths : wrappedReadPaths(words, name);
|
|
2600
|
+
if (readPaths) {
|
|
2601
|
+
for (const p of readPaths) {
|
|
2602
|
+
result = stricter(result, matchSensitivePath2(p));
|
|
2603
|
+
if (result?.verdict === "block") return false;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
for (const p of copySourcePaths(words)) {
|
|
2607
|
+
result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
|
|
2608
|
+
}
|
|
2514
2609
|
return true;
|
|
2515
2610
|
});
|
|
2516
2611
|
return result;
|
|
@@ -2518,6 +2613,192 @@ function analyzeFsOperationImpl(command) {
|
|
|
2518
2613
|
return null;
|
|
2519
2614
|
}
|
|
2520
2615
|
}
|
|
2616
|
+
function stricter(a, b) {
|
|
2617
|
+
if (!a) return b;
|
|
2618
|
+
if (!b) return a;
|
|
2619
|
+
return b.verdict === "block" && a.verdict !== "block" ? b : a;
|
|
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
|
+
}
|
|
2755
|
+
function matchSensitivePath2(p) {
|
|
2756
|
+
for (const sp of SENSITIVE_PATH_RULES) {
|
|
2757
|
+
if (sp.match(p))
|
|
2758
|
+
return { ruleName: sp.rule, verdict: sp.verdict ?? "block", reason: sp.reason, path: p };
|
|
2759
|
+
}
|
|
2760
|
+
return null;
|
|
2761
|
+
}
|
|
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);
|
|
2767
|
+
function wrappedReadPaths(words, name) {
|
|
2768
|
+
if (name === "find") {
|
|
2769
|
+
const { k, starts } = findStartPoints(words, 0);
|
|
2770
|
+
return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
|
|
2771
|
+
}
|
|
2772
|
+
if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
|
|
2773
|
+
const h = unwrapCommandHead(words);
|
|
2774
|
+
return h > 0 && isReaderWord(words[h] ?? null) ? positionalAfter(words, h + 1) : null;
|
|
2775
|
+
}
|
|
2776
|
+
function literalShellPayload(words, name) {
|
|
2777
|
+
const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
|
|
2778
|
+
const head = (words[h] ?? "").split("/").pop()?.toLowerCase() ?? "";
|
|
2779
|
+
if (head === "eval") {
|
|
2780
|
+
const rest = words.slice(h + 1);
|
|
2781
|
+
if (rest.length === 0 || rest.some((w) => w === null)) return null;
|
|
2782
|
+
return rest.join(" ");
|
|
2783
|
+
}
|
|
2784
|
+
if (SHELL_INTERPRETERS.has(head)) {
|
|
2785
|
+
const c = words.findIndex((w, i) => i > h && w !== null && isInlineCodeFlag(head, w));
|
|
2786
|
+
if (c < 0) return null;
|
|
2787
|
+
return words[c + 1] ?? null;
|
|
2788
|
+
}
|
|
2789
|
+
return null;
|
|
2790
|
+
}
|
|
2791
|
+
function jailedRedirectRead(stmt) {
|
|
2792
|
+
const redirs = stmt.Redirs || [];
|
|
2793
|
+
for (const r of redirs) {
|
|
2794
|
+
if (!r || !REDIR_FILE_IN_OPS.has(r.Op)) continue;
|
|
2795
|
+
const p = resolveWordLiteral(r.Word);
|
|
2796
|
+
if (p === null || p === "") continue;
|
|
2797
|
+
const hit = matchSensitivePath2(p);
|
|
2798
|
+
if (hit) return hit;
|
|
2799
|
+
}
|
|
2800
|
+
return null;
|
|
2801
|
+
}
|
|
2521
2802
|
function analyzeShellCommand(command) {
|
|
2522
2803
|
const actions = [];
|
|
2523
2804
|
const paths = [];
|
|
@@ -2650,21 +2931,7 @@ function evaluateEgress(dests, policy) {
|
|
|
2650
2931
|
}
|
|
2651
2932
|
return review;
|
|
2652
2933
|
}
|
|
2653
|
-
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
2654
|
-
"cat",
|
|
2655
|
-
"head",
|
|
2656
|
-
"tail",
|
|
2657
|
-
"grep",
|
|
2658
|
-
"awk",
|
|
2659
|
-
"sed",
|
|
2660
|
-
"cut",
|
|
2661
|
-
"sort",
|
|
2662
|
-
"tee",
|
|
2663
|
-
"less",
|
|
2664
|
-
"more",
|
|
2665
|
-
"strings",
|
|
2666
|
-
"xxd"
|
|
2667
|
-
]);
|
|
2934
|
+
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
|
|
2668
2935
|
var SINK_COMMANDS = /* @__PURE__ */ new Set([
|
|
2669
2936
|
"curl",
|
|
2670
2937
|
"wget",
|
|
@@ -2695,16 +2962,25 @@ var OBFUSCATORS = /* @__PURE__ */ new Set([
|
|
|
2695
2962
|
"node"
|
|
2696
2963
|
]);
|
|
2697
2964
|
var SENSITIVE_PATTERNS = [
|
|
2698
|
-
/
|
|
2699
|
-
|
|
2965
|
+
// Kept in step with the AST tier and dlp/ -- see jail-both-doors.test.ts.
|
|
2966
|
+
/(?:^|\/)\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i,
|
|
2967
|
+
// .env chain; fixtures exempt unless .local
|
|
2700
2968
|
/id_rsa|id_ed25519|id_ecdsa|id_dsa/i,
|
|
2701
2969
|
// SSH private keys
|
|
2702
2970
|
/\.pem$|\.key$|\.p12$|\.pfx$/i,
|
|
2703
2971
|
// certificate files
|
|
2704
|
-
/
|
|
2705
|
-
//
|
|
2706
|
-
/
|
|
2707
|
-
//
|
|
2972
|
+
// The `$` half mirrors shell/index.ts's SENSITIVE_PATH_RULES: a file INSIDE
|
|
2973
|
+
// the directory counts wherever it appears, while the directory ITSELF counts
|
|
2974
|
+
// only when the path is ROOTED (`~/.ssh`, `/home/u/.ssh`) -- an unrooted
|
|
2975
|
+
// `config/.ssh` is more likely a search pattern than a read. These are
|
|
2976
|
+
// extracted TOKENS (see `args.some(isSensitivePath)` below), the same input
|
|
2977
|
+
// contract as the shell tier, so the same boundary is the right one.
|
|
2978
|
+
// Without it `grep -r x ~/.ssh | curl -d @-` scored one tier BELOW the
|
|
2979
|
+
// identical pipeline naming a file inside that directory.
|
|
2980
|
+
/(?:^|\/)\.ssh\/|^(?:[~/]|[A-Za-z]:).*\/\.ssh$/i,
|
|
2981
|
+
// ~/.ssh/ and ~/.ssh
|
|
2982
|
+
/(?:^|\/)\.aws\/credentials|^(?:[~/]|[A-Za-z]:).*\/\.aws$/i,
|
|
2983
|
+
// AWS creds + dir
|
|
2708
2984
|
/(?:^|\/)\.netrc$/i,
|
|
2709
2985
|
// netrc (stores HTTP credentials)
|
|
2710
2986
|
/(?:^|\/)(passwd|shadow|sudoers)$/i,
|
|
@@ -2738,8 +3014,8 @@ function splitOnPipe(cmd) {
|
|
|
2738
3014
|
if (current.trim()) segments2.push(current.trim());
|
|
2739
3015
|
return segments2.filter(Boolean);
|
|
2740
3016
|
}
|
|
2741
|
-
function positionalTokens(
|
|
2742
|
-
return
|
|
3017
|
+
function positionalTokens(tokens) {
|
|
3018
|
+
return tokens.slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
|
|
2743
3019
|
}
|
|
2744
3020
|
function analyzePipeChain(command) {
|
|
2745
3021
|
const segments2 = splitOnPipe(command);
|
|
@@ -2762,8 +3038,10 @@ function analyzePipeChain(command) {
|
|
|
2762
3038
|
for (const segment of segments2) {
|
|
2763
3039
|
const tokens = segment.split(/\s+/).filter(Boolean);
|
|
2764
3040
|
if (tokens.length === 0) continue;
|
|
2765
|
-
const
|
|
2766
|
-
const
|
|
3041
|
+
const h = unwrapCommandHead(tokens);
|
|
3042
|
+
const head = h < tokens.length ? h : 0;
|
|
3043
|
+
const binary = tokens[head].toLowerCase();
|
|
3044
|
+
const args = positionalTokens(tokens.slice(head));
|
|
2767
3045
|
if (SOURCE_COMMANDS.has(binary)) {
|
|
2768
3046
|
sourceFiles.push(...args);
|
|
2769
3047
|
if (args.some(isSensitivePath)) hasSensitiveSource = true;
|
|
@@ -3350,6 +3628,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3350
3628
|
const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
|
|
3351
3629
|
const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
|
|
3352
3630
|
const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
|
|
3631
|
+
let pendingAstReview;
|
|
3353
3632
|
if (bashCommand !== null) {
|
|
3354
3633
|
const pipeVerdict = pipeChainVerdict(
|
|
3355
3634
|
bashCommand,
|
|
@@ -3361,7 +3640,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3361
3640
|
if (fsVerdict) {
|
|
3362
3641
|
const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
|
|
3363
3642
|
const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
|
|
3364
|
-
|
|
3643
|
+
const astVerdict = {
|
|
3365
3644
|
decision: fsVerdict.verdict,
|
|
3366
3645
|
blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
|
|
3367
3646
|
reason: fsVerdict.reason,
|
|
@@ -3369,6 +3648,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3369
3648
|
ruleName: fsVerdict.ruleName,
|
|
3370
3649
|
ruleDescription: fsVerdict.reason
|
|
3371
3650
|
};
|
|
3651
|
+
if (fsVerdict.verdict === "block") return astVerdict;
|
|
3652
|
+
pendingAstReview = astVerdict;
|
|
3372
3653
|
}
|
|
3373
3654
|
const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
|
|
3374
3655
|
const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
|
|
@@ -3411,7 +3692,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3411
3692
|
const matchedRule = resolvePinned(matches);
|
|
3412
3693
|
if (matchedRule) {
|
|
3413
3694
|
if (matchedRule.verdict === "allow")
|
|
3414
|
-
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
3695
|
+
return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
3415
3696
|
return {
|
|
3416
3697
|
decision: matchedRule.verdict,
|
|
3417
3698
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -3438,6 +3719,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
3438
3719
|
allTokens = analyzed.allTokens;
|
|
3439
3720
|
pathTokens = analyzed.paths;
|
|
3440
3721
|
const candidates = [];
|
|
3722
|
+
if (pendingAstReview) candidates.push(pendingAstReview);
|
|
3441
3723
|
const evalVerdict = detectDangerousShellExec(shellCommand);
|
|
3442
3724
|
if (evalVerdict === "block") {
|
|
3443
3725
|
return {
|
|
@@ -4162,7 +4444,7 @@ var project_jail_default = {
|
|
|
4162
4444
|
{
|
|
4163
4445
|
field: "command",
|
|
4164
4446
|
op: "matches",
|
|
4165
|
-
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[\\/\\\\]",
|
|
4447
|
+
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[\\/\\\\]",
|
|
4166
4448
|
flags: "i"
|
|
4167
4449
|
}
|
|
4168
4450
|
],
|
|
@@ -4176,7 +4458,7 @@ var project_jail_default = {
|
|
|
4176
4458
|
{
|
|
4177
4459
|
field: "command",
|
|
4178
4460
|
op: "matches",
|
|
4179
|
-
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[\\/\\\\]",
|
|
4461
|
+
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[\\/\\\\]",
|
|
4180
4462
|
flags: "i"
|
|
4181
4463
|
}
|
|
4182
4464
|
],
|
|
@@ -4190,7 +4472,7 @@ var project_jail_default = {
|
|
|
4190
4472
|
{
|
|
4191
4473
|
field: "command",
|
|
4192
4474
|
op: "matches",
|
|
4193
|
-
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|$|[;&|>)<])",
|
|
4475
|
+
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|$|[;&|>)<])",
|
|
4194
4476
|
flags: "i"
|
|
4195
4477
|
}
|
|
4196
4478
|
],
|
|
@@ -4204,7 +4486,7 @@ var project_jail_default = {
|
|
|
4204
4486
|
{
|
|
4205
4487
|
field: "command",
|
|
4206
4488
|
op: "matches",
|
|
4207
|
-
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)",
|
|
4489
|
+
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)",
|
|
4208
4490
|
flags: "i"
|
|
4209
4491
|
}
|
|
4210
4492
|
],
|
|
@@ -4218,7 +4500,7 @@ var project_jail_default = {
|
|
|
4218
4500
|
{
|
|
4219
4501
|
field: "file_path",
|
|
4220
4502
|
op: "matches",
|
|
4221
|
-
value: "(
|
|
4503
|
+
value: "([\\/\\\\]\\.ssh([\\/\\\\]|$)|^\\.ssh[\\/\\\\])",
|
|
4222
4504
|
flags: "i"
|
|
4223
4505
|
}
|
|
4224
4506
|
],
|
|
@@ -4232,7 +4514,7 @@ var project_jail_default = {
|
|
|
4232
4514
|
{
|
|
4233
4515
|
field: "file_path",
|
|
4234
4516
|
op: "matches",
|
|
4235
|
-
value: "(
|
|
4517
|
+
value: "([\\/\\\\]\\.aws([\\/\\\\]|$)|^\\.aws[\\/\\\\])",
|
|
4236
4518
|
flags: "i"
|
|
4237
4519
|
}
|
|
4238
4520
|
],
|
|
@@ -4246,7 +4528,7 @@ var project_jail_default = {
|
|
|
4246
4528
|
{
|
|
4247
4529
|
field: "file_path",
|
|
4248
4530
|
op: "matches",
|
|
4249
|
-
value: "(^|[\\/\\\\])\\.env(
|
|
4531
|
+
value: "(^|[\\/\\\\])\\.env(?![\\w-])(?:[\\w.-]*\\.local$|(?!\\.(example|sample|template)\\b)(?!\\.test$)[\\w.-]*$)",
|
|
4250
4532
|
flags: "i"
|
|
4251
4533
|
}
|
|
4252
4534
|
],
|
|
@@ -7023,12 +7305,13 @@ function extractFilePaths(toolName, args) {
|
|
|
7023
7305
|
}
|
|
7024
7306
|
return paths.filter(Boolean);
|
|
7025
7307
|
}
|
|
7308
|
+
var NETWORK_COMMAND_RE = new RegExp(`(?<![.\\w-])(${[...NET_BINARIES].join("|")})\\b`);
|
|
7026
7309
|
function isNetworkTool(toolName, args) {
|
|
7027
7310
|
const t = toolName.toLowerCase();
|
|
7028
7311
|
if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal.execute") {
|
|
7029
7312
|
const a = args;
|
|
7030
7313
|
const cmd = typeof a?.command === "string" ? a.command : typeof a?.cmd === "string" ? a.cmd : "";
|
|
7031
|
-
return
|
|
7314
|
+
return NETWORK_COMMAND_RE.test(cmd);
|
|
7032
7315
|
}
|
|
7033
7316
|
return false;
|
|
7034
7317
|
}
|