@node9/proxy 2.12.0 → 2.13.1
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 +543 -514
- package/dist/cli.mjs +543 -514
- package/dist/dashboard.mjs +2580 -2327
- package/dist/index.js +169 -84
- package/dist/index.mjs +169 -84
- package/dist/scan-ink.mjs +43 -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",
|
|
@@ -1648,7 +1671,11 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
1648
1671
|
"dd"
|
|
1649
1672
|
]);
|
|
1650
1673
|
var FS_OP_PRESCREEN_RE = new RegExp(
|
|
1651
|
-
|
|
1674
|
+
// A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
|
|
1675
|
+
// reader right after `"` / `'`, and without these two characters the
|
|
1676
|
+
// prescreen rejected every string-wrapped read before the parser ran.
|
|
1677
|
+
// 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|(?<!<)<(?!<)`
|
|
1652
1679
|
);
|
|
1653
1680
|
var HOME_CACHE_ALLOWLIST = [
|
|
1654
1681
|
".cache",
|
|
@@ -1669,12 +1696,12 @@ var SENSITIVE_PATH_RULES = [
|
|
|
1669
1696
|
{
|
|
1670
1697
|
rule: "shield:project-jail:block-read-ssh",
|
|
1671
1698
|
reason: "Reading SSH private keys is blocked by project-jail shield",
|
|
1672
|
-
match: (p) => /(
|
|
1699
|
+
match: (p) => /([\\/]\.ssh[\\/]|^\.ssh[\\/]|^(?:[~/]|[A-Za-z]:).*[\\/]\.ssh$)/i.test(p)
|
|
1673
1700
|
},
|
|
1674
1701
|
{
|
|
1675
1702
|
rule: "shield:project-jail:block-read-aws",
|
|
1676
1703
|
reason: "Reading AWS credentials is blocked by project-jail shield",
|
|
1677
|
-
match: (p) => /(
|
|
1704
|
+
match: (p) => /([\\/]\.aws[\\/]|^\.aws[\\/]|^(?:[~/]|[A-Za-z]:).*[\\/]\.aws$)/i.test(p)
|
|
1678
1705
|
},
|
|
1679
1706
|
{
|
|
1680
1707
|
// Mirrors the JSON shield's `.env` pattern (project-jail.json's
|
|
@@ -1718,7 +1745,9 @@ var SENSITIVE_PATH_RULES = [
|
|
|
1718
1745
|
// symmetry — silently exempts every `.env.test.*` file.
|
|
1719
1746
|
//
|
|
1720
1747
|
// shields.test.ts:983-995 is the canonical contract; keep both in step.
|
|
1721
|
-
match: (p) => /(?:^|[\\/])\.env(?![\w-])(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]
|
|
1748
|
+
match: (p) => /(?:^|[\\/])\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i.test(
|
|
1749
|
+
p
|
|
1750
|
+
)
|
|
1722
1751
|
},
|
|
1723
1752
|
{
|
|
1724
1753
|
// verdict: 'review' (not 'block') is a deliberate design choice
|
|
@@ -1929,15 +1958,14 @@ function listOps() {
|
|
|
1929
1958
|
return _listOps;
|
|
1930
1959
|
}
|
|
1931
1960
|
var WRAPPER_TAKES_TARGET = /* @__PURE__ */ new Set(["chroot"]);
|
|
1961
|
+
var FIND_EXEC_FLAGS = /* @__PURE__ */ new Set(["-exec", "-execdir", "-ok", "-okdir"]);
|
|
1932
1962
|
var INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
1933
1963
|
function unwrapCommandHead(words) {
|
|
1934
1964
|
let i = 0;
|
|
1935
1965
|
while (i < words.length) {
|
|
1936
1966
|
const head = (words[i] ?? "").toLowerCase().split("/").pop() ?? "";
|
|
1937
1967
|
if (head === "find") {
|
|
1938
|
-
const x = words.findIndex(
|
|
1939
|
-
(w, k) => k > i && (w === "-exec" || w === "-execdir" || w === "-ok")
|
|
1940
|
-
);
|
|
1968
|
+
const x = words.findIndex((w, k) => k > i && w !== null && FIND_EXEC_FLAGS.has(w));
|
|
1941
1969
|
if (x < 0) break;
|
|
1942
1970
|
i = x + 1;
|
|
1943
1971
|
continue;
|
|
@@ -1959,7 +1987,11 @@ function unwrapCommandHead(words) {
|
|
|
1959
1987
|
if (t.startsWith("-")) {
|
|
1960
1988
|
i++;
|
|
1961
1989
|
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())
|
|
1990
|
+
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`,
|
|
1991
|
+
// `stdbuf -o0 cat X`, `ionice -c3 cat X`. Without this the head was
|
|
1992
|
+
// swallowed and the jail needed a looser fallback whose cost was a
|
|
1993
|
+
// false positive on `sudo echo cat X`.
|
|
1994
|
+
!FS_READ_TOOLS.has(nxt.split("/").pop()?.toLowerCase() ?? ""))
|
|
1963
1995
|
i++;
|
|
1964
1996
|
continue;
|
|
1965
1997
|
}
|
|
@@ -2080,36 +2112,29 @@ function isProtectedHomePath(rawPath) {
|
|
|
2080
2112
|
}
|
|
2081
2113
|
function extractLiteralArgs(callExpr) {
|
|
2082
2114
|
const args = callExpr.Args || [];
|
|
2083
|
-
if (args.length === 0) return { name: "", flags: [], paths: [] };
|
|
2084
|
-
const
|
|
2085
|
-
|
|
2086
|
-
let s = "";
|
|
2087
|
-
for (const p of parts) {
|
|
2088
|
-
const t = syntax.NodeType(p);
|
|
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
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
return s;
|
|
2100
|
-
};
|
|
2101
|
-
const name = (litFromWord(args[0]) || "").toLowerCase();
|
|
2115
|
+
if (args.length === 0) return { name: "", flags: [], paths: [], words: [] };
|
|
2116
|
+
const words = args.map((a) => resolveWordLiteral(a));
|
|
2117
|
+
const name = (words[0] ?? "").toLowerCase();
|
|
2102
2118
|
const flags = [];
|
|
2103
2119
|
const paths = [];
|
|
2104
|
-
for (let i = 1; i <
|
|
2105
|
-
const v =
|
|
2120
|
+
for (let i = 1; i < words.length; i++) {
|
|
2121
|
+
const v = words[i];
|
|
2106
2122
|
if (v === null) continue;
|
|
2107
2123
|
if (v.startsWith("-")) flags.push(v);
|
|
2108
2124
|
else paths.push(v);
|
|
2109
2125
|
}
|
|
2110
|
-
return { name, flags, paths };
|
|
2126
|
+
return { name, flags, paths, words };
|
|
2111
2127
|
}
|
|
2112
|
-
var NET_BINARIES = /* @__PURE__ */ new Set([
|
|
2128
|
+
var NET_BINARIES = /* @__PURE__ */ new Set([
|
|
2129
|
+
"curl",
|
|
2130
|
+
"wget",
|
|
2131
|
+
"scp",
|
|
2132
|
+
"ssh",
|
|
2133
|
+
"nc",
|
|
2134
|
+
"ncat",
|
|
2135
|
+
"netcat",
|
|
2136
|
+
"rsync"
|
|
2137
|
+
]);
|
|
2113
2138
|
var VALUE_FLAGS = {
|
|
2114
2139
|
curl: /* @__PURE__ */ new Set([
|
|
2115
2140
|
"-d",
|
|
@@ -2394,6 +2419,9 @@ function deriveRedirOp(sample) {
|
|
|
2394
2419
|
}
|
|
2395
2420
|
}
|
|
2396
2421
|
var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
|
|
2422
|
+
var REDIR_FILE_IN_OPS = new Set(
|
|
2423
|
+
[deriveRedirOp("cat < f"), deriveRedirOp("cat <> f")].filter((op) => op >= 0)
|
|
2424
|
+
);
|
|
2397
2425
|
var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
|
|
2398
2426
|
deriveRedirOp("cat <<X\nX"),
|
|
2399
2427
|
deriveRedirOp("cat <<-X\nX")
|
|
@@ -2458,16 +2486,21 @@ function isRmCreatedInCommandCleanup(command) {
|
|
|
2458
2486
|
}
|
|
2459
2487
|
return sawRm && ok;
|
|
2460
2488
|
}
|
|
2461
|
-
function analyzeFsOperationImpl(command) {
|
|
2489
|
+
function analyzeFsOperationImpl(command, depth = 0) {
|
|
2462
2490
|
const f = parseShared(command);
|
|
2463
2491
|
if (f === PARSE_FAIL) return null;
|
|
2464
2492
|
let result = null;
|
|
2465
2493
|
try {
|
|
2466
2494
|
syntax.Walk(f, (node) => {
|
|
2467
|
-
if (!node || result) return false;
|
|
2495
|
+
if (!node || result?.verdict === "block") return false;
|
|
2468
2496
|
const n = node;
|
|
2469
|
-
|
|
2470
|
-
|
|
2497
|
+
const nodeType = syntax.NodeType(n);
|
|
2498
|
+
if (nodeType === "Stmt") {
|
|
2499
|
+
result = stricter(result, jailedRedirectRead(n));
|
|
2500
|
+
return result?.verdict !== "block";
|
|
2501
|
+
}
|
|
2502
|
+
if (nodeType !== "CallExpr") return true;
|
|
2503
|
+
const { name, flags, paths, words } = extractLiteralArgs(n);
|
|
2471
2504
|
if (!name) return true;
|
|
2472
2505
|
if (name === "rm") {
|
|
2473
2506
|
const flagStr = flags.join("").toLowerCase();
|
|
@@ -2496,19 +2529,22 @@ function analyzeFsOperationImpl(command) {
|
|
|
2496
2529
|
}
|
|
2497
2530
|
}
|
|
2498
2531
|
}
|
|
2499
|
-
if (
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
reason: sp.reason,
|
|
2507
|
-
path: p
|
|
2508
|
-
};
|
|
2509
|
-
return false;
|
|
2510
|
-
}
|
|
2532
|
+
if (depth < 1) {
|
|
2533
|
+
const payload = literalShellPayload(words, name);
|
|
2534
|
+
if (payload !== null) {
|
|
2535
|
+
const inner = analyzeFsOperationImpl(payload, depth + 1);
|
|
2536
|
+
if (inner) {
|
|
2537
|
+
result = inner;
|
|
2538
|
+
return false;
|
|
2511
2539
|
}
|
|
2540
|
+
return true;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
const readPaths = FS_READ_TOOLS.has(name) ? paths : wrappedReadPaths(words, name);
|
|
2544
|
+
if (readPaths) {
|
|
2545
|
+
for (const p of readPaths) {
|
|
2546
|
+
result = stricter(result, matchSensitivePath2(p));
|
|
2547
|
+
if (result?.verdict === "block") return false;
|
|
2512
2548
|
}
|
|
2513
2549
|
}
|
|
2514
2550
|
return true;
|
|
@@ -2518,6 +2554,57 @@ function analyzeFsOperationImpl(command) {
|
|
|
2518
2554
|
return null;
|
|
2519
2555
|
}
|
|
2520
2556
|
}
|
|
2557
|
+
function stricter(a, b) {
|
|
2558
|
+
if (!a) return b;
|
|
2559
|
+
if (!b) return a;
|
|
2560
|
+
return b.verdict === "block" && a.verdict !== "block" ? b : a;
|
|
2561
|
+
}
|
|
2562
|
+
function matchSensitivePath2(p) {
|
|
2563
|
+
for (const sp of SENSITIVE_PATH_RULES) {
|
|
2564
|
+
if (sp.match(p))
|
|
2565
|
+
return { ruleName: sp.rule, verdict: sp.verdict ?? "block", reason: sp.reason, path: p };
|
|
2566
|
+
}
|
|
2567
|
+
return null;
|
|
2568
|
+
}
|
|
2569
|
+
var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(w.split("/").pop()?.toLowerCase() ?? "");
|
|
2570
|
+
var positionalAfter = (words, from, to = words.length) => words.slice(from, to).filter((w) => w !== null && !w.startsWith("-"));
|
|
2571
|
+
function wrappedReadPaths(words, name) {
|
|
2572
|
+
if (name === "find") {
|
|
2573
|
+
const k = words.findIndex((w) => w !== null && FIND_EXEC_FLAGS.has(w));
|
|
2574
|
+
if (k < 1 || !isReaderWord(words[k + 1] ?? null)) return null;
|
|
2575
|
+
const firstPredicate = words.findIndex((w, i) => i > 0 && w !== null && w.startsWith("-"));
|
|
2576
|
+
return positionalAfter(words, 1, firstPredicate > 0 ? firstPredicate : k);
|
|
2577
|
+
}
|
|
2578
|
+
if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
|
|
2579
|
+
const h = unwrapCommandHead(words);
|
|
2580
|
+
return h > 0 && isReaderWord(words[h] ?? null) ? positionalAfter(words, h + 1) : null;
|
|
2581
|
+
}
|
|
2582
|
+
function literalShellPayload(words, name) {
|
|
2583
|
+
const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
|
|
2584
|
+
const head = (words[h] ?? "").split("/").pop()?.toLowerCase() ?? "";
|
|
2585
|
+
if (head === "eval") {
|
|
2586
|
+
const rest = words.slice(h + 1);
|
|
2587
|
+
if (rest.length === 0 || rest.some((w) => w === null)) return null;
|
|
2588
|
+
return rest.join(" ");
|
|
2589
|
+
}
|
|
2590
|
+
if (SHELL_INTERPRETERS.has(head)) {
|
|
2591
|
+
const c = words.findIndex((w, i) => i > h && w !== null && isInlineCodeFlag(head, w));
|
|
2592
|
+
if (c < 0) return null;
|
|
2593
|
+
return words[c + 1] ?? null;
|
|
2594
|
+
}
|
|
2595
|
+
return null;
|
|
2596
|
+
}
|
|
2597
|
+
function jailedRedirectRead(stmt) {
|
|
2598
|
+
const redirs = stmt.Redirs || [];
|
|
2599
|
+
for (const r of redirs) {
|
|
2600
|
+
if (!r || !REDIR_FILE_IN_OPS.has(r.Op)) continue;
|
|
2601
|
+
const p = resolveWordLiteral(r.Word);
|
|
2602
|
+
if (p === null || p === "") continue;
|
|
2603
|
+
const hit = matchSensitivePath2(p);
|
|
2604
|
+
if (hit) return hit;
|
|
2605
|
+
}
|
|
2606
|
+
return null;
|
|
2607
|
+
}
|
|
2521
2608
|
function analyzeShellCommand(command) {
|
|
2522
2609
|
const actions = [];
|
|
2523
2610
|
const paths = [];
|
|
@@ -2650,21 +2737,7 @@ function evaluateEgress(dests, policy) {
|
|
|
2650
2737
|
}
|
|
2651
2738
|
return review;
|
|
2652
2739
|
}
|
|
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
|
-
]);
|
|
2740
|
+
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
|
|
2668
2741
|
var SINK_COMMANDS = /* @__PURE__ */ new Set([
|
|
2669
2742
|
"curl",
|
|
2670
2743
|
"wget",
|
|
@@ -2695,16 +2768,25 @@ var OBFUSCATORS = /* @__PURE__ */ new Set([
|
|
|
2695
2768
|
"node"
|
|
2696
2769
|
]);
|
|
2697
2770
|
var SENSITIVE_PATTERNS = [
|
|
2698
|
-
/
|
|
2699
|
-
|
|
2771
|
+
// Kept in step with the AST tier and dlp/ -- see jail-both-doors.test.ts.
|
|
2772
|
+
/(?:^|\/)\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i,
|
|
2773
|
+
// .env chain; fixtures exempt unless .local
|
|
2700
2774
|
/id_rsa|id_ed25519|id_ecdsa|id_dsa/i,
|
|
2701
2775
|
// SSH private keys
|
|
2702
2776
|
/\.pem$|\.key$|\.p12$|\.pfx$/i,
|
|
2703
2777
|
// certificate files
|
|
2704
|
-
/
|
|
2705
|
-
//
|
|
2706
|
-
/
|
|
2707
|
-
//
|
|
2778
|
+
// The `$` half mirrors shell/index.ts's SENSITIVE_PATH_RULES: a file INSIDE
|
|
2779
|
+
// the directory counts wherever it appears, while the directory ITSELF counts
|
|
2780
|
+
// only when the path is ROOTED (`~/.ssh`, `/home/u/.ssh`) -- an unrooted
|
|
2781
|
+
// `config/.ssh` is more likely a search pattern than a read. These are
|
|
2782
|
+
// extracted TOKENS (see `args.some(isSensitivePath)` below), the same input
|
|
2783
|
+
// contract as the shell tier, so the same boundary is the right one.
|
|
2784
|
+
// Without it `grep -r x ~/.ssh | curl -d @-` scored one tier BELOW the
|
|
2785
|
+
// identical pipeline naming a file inside that directory.
|
|
2786
|
+
/(?:^|\/)\.ssh\/|^(?:[~/]|[A-Za-z]:).*\/\.ssh$/i,
|
|
2787
|
+
// ~/.ssh/ and ~/.ssh
|
|
2788
|
+
/(?:^|\/)\.aws\/credentials|^(?:[~/]|[A-Za-z]:).*\/\.aws$/i,
|
|
2789
|
+
// AWS creds + dir
|
|
2708
2790
|
/(?:^|\/)\.netrc$/i,
|
|
2709
2791
|
// netrc (stores HTTP credentials)
|
|
2710
2792
|
/(?:^|\/)(passwd|shadow|sudoers)$/i,
|
|
@@ -2738,8 +2820,8 @@ function splitOnPipe(cmd) {
|
|
|
2738
2820
|
if (current.trim()) segments2.push(current.trim());
|
|
2739
2821
|
return segments2.filter(Boolean);
|
|
2740
2822
|
}
|
|
2741
|
-
function positionalTokens(
|
|
2742
|
-
return
|
|
2823
|
+
function positionalTokens(tokens) {
|
|
2824
|
+
return tokens.slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
|
|
2743
2825
|
}
|
|
2744
2826
|
function analyzePipeChain(command) {
|
|
2745
2827
|
const segments2 = splitOnPipe(command);
|
|
@@ -2762,8 +2844,10 @@ function analyzePipeChain(command) {
|
|
|
2762
2844
|
for (const segment of segments2) {
|
|
2763
2845
|
const tokens = segment.split(/\s+/).filter(Boolean);
|
|
2764
2846
|
if (tokens.length === 0) continue;
|
|
2765
|
-
const
|
|
2766
|
-
const
|
|
2847
|
+
const h = unwrapCommandHead(tokens);
|
|
2848
|
+
const head = h < tokens.length ? h : 0;
|
|
2849
|
+
const binary = tokens[head].toLowerCase();
|
|
2850
|
+
const args = positionalTokens(tokens.slice(head));
|
|
2767
2851
|
if (SOURCE_COMMANDS.has(binary)) {
|
|
2768
2852
|
sourceFiles.push(...args);
|
|
2769
2853
|
if (args.some(isSensitivePath)) hasSensitiveSource = true;
|
|
@@ -4162,7 +4246,7 @@ var project_jail_default = {
|
|
|
4162
4246
|
{
|
|
4163
4247
|
field: "command",
|
|
4164
4248
|
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[\\/\\\\]",
|
|
4249
|
+
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
4250
|
flags: "i"
|
|
4167
4251
|
}
|
|
4168
4252
|
],
|
|
@@ -4176,7 +4260,7 @@ var project_jail_default = {
|
|
|
4176
4260
|
{
|
|
4177
4261
|
field: "command",
|
|
4178
4262
|
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[\\/\\\\]",
|
|
4263
|
+
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
4264
|
flags: "i"
|
|
4181
4265
|
}
|
|
4182
4266
|
],
|
|
@@ -4190,7 +4274,7 @@ var project_jail_default = {
|
|
|
4190
4274
|
{
|
|
4191
4275
|
field: "command",
|
|
4192
4276
|
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|$|[;&|>)<])",
|
|
4277
|
+
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
4278
|
flags: "i"
|
|
4195
4279
|
}
|
|
4196
4280
|
],
|
|
@@ -4204,7 +4288,7 @@ var project_jail_default = {
|
|
|
4204
4288
|
{
|
|
4205
4289
|
field: "command",
|
|
4206
4290
|
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)",
|
|
4291
|
+
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
4292
|
flags: "i"
|
|
4209
4293
|
}
|
|
4210
4294
|
],
|
|
@@ -4218,7 +4302,7 @@ var project_jail_default = {
|
|
|
4218
4302
|
{
|
|
4219
4303
|
field: "file_path",
|
|
4220
4304
|
op: "matches",
|
|
4221
|
-
value: "(
|
|
4305
|
+
value: "([\\/\\\\]\\.ssh([\\/\\\\]|$)|^\\.ssh[\\/\\\\])",
|
|
4222
4306
|
flags: "i"
|
|
4223
4307
|
}
|
|
4224
4308
|
],
|
|
@@ -4232,7 +4316,7 @@ var project_jail_default = {
|
|
|
4232
4316
|
{
|
|
4233
4317
|
field: "file_path",
|
|
4234
4318
|
op: "matches",
|
|
4235
|
-
value: "(
|
|
4319
|
+
value: "([\\/\\\\]\\.aws([\\/\\\\]|$)|^\\.aws[\\/\\\\])",
|
|
4236
4320
|
flags: "i"
|
|
4237
4321
|
}
|
|
4238
4322
|
],
|
|
@@ -4246,7 +4330,7 @@ var project_jail_default = {
|
|
|
4246
4330
|
{
|
|
4247
4331
|
field: "file_path",
|
|
4248
4332
|
op: "matches",
|
|
4249
|
-
value: "(^|[\\/\\\\])\\.env(
|
|
4333
|
+
value: "(^|[\\/\\\\])\\.env(?![\\w-])(?:[\\w.-]*\\.local$|(?!\\.(example|sample|template)\\b)(?!\\.test$)[\\w.-]*$)",
|
|
4250
4334
|
flags: "i"
|
|
4251
4335
|
}
|
|
4252
4336
|
],
|
|
@@ -7023,12 +7107,13 @@ function extractFilePaths(toolName, args) {
|
|
|
7023
7107
|
}
|
|
7024
7108
|
return paths.filter(Boolean);
|
|
7025
7109
|
}
|
|
7110
|
+
var NETWORK_COMMAND_RE = new RegExp(`(?<![.\\w-])(${[...NET_BINARIES].join("|")})\\b`);
|
|
7026
7111
|
function isNetworkTool(toolName, args) {
|
|
7027
7112
|
const t = toolName.toLowerCase();
|
|
7028
7113
|
if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal.execute") {
|
|
7029
7114
|
const a = args;
|
|
7030
7115
|
const cmd = typeof a?.command === "string" ? a.command : typeof a?.cmd === "string" ? a.cmd : "";
|
|
7031
|
-
return
|
|
7116
|
+
return NETWORK_COMMAND_RE.test(cmd);
|
|
7032
7117
|
}
|
|
7033
7118
|
return false;
|
|
7034
7119
|
}
|
package/dist/scan-ink.mjs
CHANGED
|
@@ -709,13 +709,32 @@ var DLP_PATTERNS_GLOBAL = DLP_PATTERNS.map(
|
|
|
709
709
|
})
|
|
710
710
|
);
|
|
711
711
|
var SENSITIVE_PATH_PATTERNS = [
|
|
712
|
-
/[/\\]\.ssh[/\\]/i,
|
|
713
|
-
/[/\\]\.aws[/\\]/i,
|
|
712
|
+
/[/\\]\.ssh([/\\]|$)/i,
|
|
713
|
+
/[/\\]\.aws([/\\]|$)/i,
|
|
714
714
|
/[/\\]\.config[/\\]gcloud[/\\]/i,
|
|
715
715
|
/[/\\]\.azure[/\\]/i,
|
|
716
716
|
/[/\\]\.kube[/\\]config$/i,
|
|
717
|
-
|
|
718
|
-
// .
|
|
717
|
+
// ⚠️ ONE SEMANTIC, FOUR COPIES. This is the AST tier's `.env` rule verbatim
|
|
718
|
+
// (shell/index.ts SENSITIVE_PATH_RULES), whose reasoning is documented there:
|
|
719
|
+
// structural suffix chain rather than a hand-written list, `example|sample|
|
|
720
|
+
// template` exempt because a fixture stays a fixture whatever follows, and
|
|
721
|
+
// `.test` anchored because `test` names an ENVIRONMENT -- `.env.test` is the
|
|
722
|
+
// committed template, `.env.test.local` is gitignored and holds real values.
|
|
723
|
+
//
|
|
724
|
+
// It was previously `[/\\]\.env($|\.)` with NO exemptions, so `Read .env.example`
|
|
725
|
+
// blocked while `cat .env.example` allowed: the same file, opposite verdicts,
|
|
726
|
+
// decided only by which tool asked. See src/__tests__/jail-both-doors.test.ts,
|
|
727
|
+
// which is the contract that now holds these copies in step, and stage 5 of
|
|
728
|
+
// doc/credential-jail-architecture.md, which replaces them with one generated
|
|
729
|
+
// source.
|
|
730
|
+
// ⚠️ The `.local` branch comes FIRST and takes no exemption. A fixture stays a
|
|
731
|
+
// fixture whatever follows it -- `.env.example.md` is documentation -- but
|
|
732
|
+
// `.env.example.local` is gitignored by the `.env*.local` convention and holds
|
|
733
|
+
// real values, exactly the reasoning that anchors `(?!\.test$)` rather than
|
|
734
|
+
// using `\b`. Without this branch the fixture exemption also bought a two-step
|
|
735
|
+
// bypass: `cp .env .env.sample`, then read the copy.
|
|
736
|
+
/[/\\]\.env(?![\w-])(?:[\w.-]*\.local$|(?!\.(?:example|sample|template)\b)(?!\.test$)[\w.-]*$)/i,
|
|
737
|
+
// .env + any suffix chain; fixtures exempt unless .local
|
|
719
738
|
/[/\\]\.git-credentials$/i,
|
|
720
739
|
/[/\\]\.npmrc$/i,
|
|
721
740
|
/[/\\]\.docker[/\\]config\.json$/i,
|
|
@@ -782,6 +801,10 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
782
801
|
"od",
|
|
783
802
|
"xxd",
|
|
784
803
|
"hexdump",
|
|
804
|
+
// Emits the file's bytes, re-encoded, so it is a read by the set's own test
|
|
805
|
+
// ("does it emit file contents"). Absent until 2026-09-10, which is why
|
|
806
|
+
// `base64 ~/.ssh/id_rsa` printed a private key with no verdict.
|
|
807
|
+
"base64",
|
|
785
808
|
"strings",
|
|
786
809
|
"sort",
|
|
787
810
|
"uniq",
|
|
@@ -790,7 +813,11 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
|
790
813
|
"dd"
|
|
791
814
|
]);
|
|
792
815
|
var FS_OP_PRESCREEN_RE = new RegExp(
|
|
793
|
-
|
|
816
|
+
// A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
|
|
817
|
+
// reader right after `"` / `'`, and without these two characters the
|
|
818
|
+
// prescreen rejected every string-wrapped read before the parser ran.
|
|
819
|
+
// Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
|
|
820
|
+
`(?:^|[\\s|;&("'\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
|
|
794
821
|
);
|
|
795
822
|
function deriveRedirOp(sample) {
|
|
796
823
|
try {
|
|
@@ -807,10 +834,14 @@ function deriveRedirOp(sample) {
|
|
|
807
834
|
}
|
|
808
835
|
}
|
|
809
836
|
var REDIR_TRUNCATE_OPS = /* @__PURE__ */ new Set([deriveRedirOp(">_f")]);
|
|
837
|
+
var REDIR_FILE_IN_OPS = new Set(
|
|
838
|
+
[deriveRedirOp("cat < f"), deriveRedirOp("cat <> f")].filter((op) => op >= 0)
|
|
839
|
+
);
|
|
810
840
|
var REDIR_HEREDOC_OPS = /* @__PURE__ */ new Set([
|
|
811
841
|
deriveRedirOp("cat <<X\nX"),
|
|
812
842
|
deriveRedirOp("cat <<-X\nX")
|
|
813
843
|
]);
|
|
844
|
+
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([...FS_READ_TOOLS, "tee"]);
|
|
814
845
|
var aws_default = {
|
|
815
846
|
name: "aws",
|
|
816
847
|
description: "Protects AWS infrastructure from destructive AI operations",
|
|
@@ -1362,7 +1393,7 @@ var project_jail_default = {
|
|
|
1362
1393
|
{
|
|
1363
1394
|
field: "command",
|
|
1364
1395
|
op: "matches",
|
|
1365
|
-
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[\\/\\\\]",
|
|
1396
|
+
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[\\/\\\\]",
|
|
1366
1397
|
flags: "i"
|
|
1367
1398
|
}
|
|
1368
1399
|
],
|
|
@@ -1376,7 +1407,7 @@ var project_jail_default = {
|
|
|
1376
1407
|
{
|
|
1377
1408
|
field: "command",
|
|
1378
1409
|
op: "matches",
|
|
1379
|
-
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[\\/\\\\]",
|
|
1410
|
+
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[\\/\\\\]",
|
|
1380
1411
|
flags: "i"
|
|
1381
1412
|
}
|
|
1382
1413
|
],
|
|
@@ -1390,7 +1421,7 @@ var project_jail_default = {
|
|
|
1390
1421
|
{
|
|
1391
1422
|
field: "command",
|
|
1392
1423
|
op: "matches",
|
|
1393
|
-
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|$|[;&|>)<])",
|
|
1424
|
+
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|$|[;&|>)<])",
|
|
1394
1425
|
flags: "i"
|
|
1395
1426
|
}
|
|
1396
1427
|
],
|
|
@@ -1404,7 +1435,7 @@ var project_jail_default = {
|
|
|
1404
1435
|
{
|
|
1405
1436
|
field: "command",
|
|
1406
1437
|
op: "matches",
|
|
1407
|
-
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)",
|
|
1438
|
+
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)",
|
|
1408
1439
|
flags: "i"
|
|
1409
1440
|
}
|
|
1410
1441
|
],
|
|
@@ -1418,7 +1449,7 @@ var project_jail_default = {
|
|
|
1418
1449
|
{
|
|
1419
1450
|
field: "file_path",
|
|
1420
1451
|
op: "matches",
|
|
1421
|
-
value: "(
|
|
1452
|
+
value: "([\\/\\\\]\\.ssh([\\/\\\\]|$)|^\\.ssh[\\/\\\\])",
|
|
1422
1453
|
flags: "i"
|
|
1423
1454
|
}
|
|
1424
1455
|
],
|
|
@@ -1432,7 +1463,7 @@ var project_jail_default = {
|
|
|
1432
1463
|
{
|
|
1433
1464
|
field: "file_path",
|
|
1434
1465
|
op: "matches",
|
|
1435
|
-
value: "(
|
|
1466
|
+
value: "([\\/\\\\]\\.aws([\\/\\\\]|$)|^\\.aws[\\/\\\\])",
|
|
1436
1467
|
flags: "i"
|
|
1437
1468
|
}
|
|
1438
1469
|
],
|
|
@@ -1446,7 +1477,7 @@ var project_jail_default = {
|
|
|
1446
1477
|
{
|
|
1447
1478
|
field: "file_path",
|
|
1448
1479
|
op: "matches",
|
|
1449
|
-
value: "(^|[\\/\\\\])\\.env(
|
|
1480
|
+
value: "(^|[\\/\\\\])\\.env(?![\\w-])(?:[\\w.-]*\\.local$|(?!\\.(example|sample|template)\\b)(?!\\.test$)[\\w.-]*$)",
|
|
1450
1481
|
flags: "i"
|
|
1451
1482
|
}
|
|
1452
1483
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.1",
|
|
4
4
|
"description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|