@node9/proxy 2.22.1 → 2.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1595,6 +1595,7 @@ function normalizeCommandForPolicyImpl(command) {
1595
1595
  const source = command.slice(s, e);
1596
1596
  if (resolved === source) continue;
1597
1597
  if (resolved === "" || /\s/.test(resolved)) continue;
1598
+ if (/^[;&|()<>]+$/.test(resolved)) continue;
1598
1599
  rewrites.push([s, e, resolved]);
1599
1600
  const quoteOnly = source.replace(/['"]/g, "");
1600
1601
  if (quoteOnly !== source) quoteOnlyRewrites.push([s, e, quoteOnly]);
@@ -2611,6 +2612,60 @@ var NET_BINARIES = /* @__PURE__ */ new Set([
2611
2612
  "rsync"
2612
2613
  ]);
2613
2614
  var VALUE_FLAGS = {
2615
+ // rsync 3.2.7, its own --help: every flag whose operand could be mistaken for
2616
+ // a host. `-e ssh` is the one that matters most (`ssh` is not the destination).
2617
+ rsync: /* @__PURE__ */ new Set([
2618
+ "-e",
2619
+ "--rsh",
2620
+ "-f",
2621
+ "--filter",
2622
+ "-T",
2623
+ "--temp-dir",
2624
+ "-B",
2625
+ "--block-size",
2626
+ "-M",
2627
+ "--remote-option",
2628
+ "--exclude",
2629
+ "--exclude-from",
2630
+ "--include",
2631
+ "--include-from",
2632
+ "--files-from",
2633
+ "--compare-dest",
2634
+ "--copy-dest",
2635
+ "--link-dest",
2636
+ "--partial-dir",
2637
+ "--log-file",
2638
+ "--password-file",
2639
+ "--bwlimit",
2640
+ "--timeout",
2641
+ "--contimeout",
2642
+ "--port",
2643
+ "--sockopts",
2644
+ "--address",
2645
+ "--chmod",
2646
+ "--chown",
2647
+ "--max-size",
2648
+ "--min-size",
2649
+ "--modify-window",
2650
+ "--out-format",
2651
+ "--log-file-format",
2652
+ "--backup-dir",
2653
+ "--suffix",
2654
+ "--iconv",
2655
+ "--max-delete",
2656
+ "--checksum-choice",
2657
+ "--info",
2658
+ "--debug",
2659
+ "--stderr",
2660
+ "--outbuf",
2661
+ "--skip-compress",
2662
+ "--usermap",
2663
+ "--groupmap",
2664
+ "--mkpath",
2665
+ "--write-batch",
2666
+ "--read-batch",
2667
+ "--only-write-batch"
2668
+ ]),
2614
2669
  curl: /* @__PURE__ */ new Set([
2615
2670
  "-d",
2616
2671
  "--data",
@@ -2694,23 +2749,163 @@ var VALUE_FLAGS = {
2694
2749
  ]),
2695
2750
  nc: /* @__PURE__ */ new Set(["-p", "-s", "-w", "-X", "-x", "-e", "-g", "-G", "-i", "-O", "-T", "-q", "-m"])
2696
2751
  };
2752
+ var HOME_VARIABLES = /* @__PURE__ */ new Set(["HOME", "USERPROFILE"]);
2753
+ var assignmentTable = null;
2754
+ var currentStmtOffset = Number.MAX_SAFE_INTEGER;
2755
+ var PAYLOAD_BUDGET = 256;
2756
+ var payloadBudget = 0;
2757
+ var seenPayloads = null;
2758
+ function payloadKey(payload) {
2759
+ if (!assignmentTable || assignmentTable.size === 0) return payload;
2760
+ const bindings = [];
2761
+ for (const [name, rec] of assignmentTable) {
2762
+ if (rec.value !== null) bindings.push(`${name}=${rec.value}`);
2763
+ }
2764
+ return `${payload}\0${bindings.sort().join("")}`;
2765
+ }
2766
+ function claimPayload(payload) {
2767
+ if (!seenPayloads) return "ok";
2768
+ const key = payloadKey(payload);
2769
+ if (seenPayloads.has(key)) return "seen";
2770
+ if (payloadBudget <= 0) return "exhausted";
2771
+ payloadBudget--;
2772
+ seenPayloads.add(key);
2773
+ return "ok";
2774
+ }
2775
+ var UNANALYSABLE_NESTING = {
2776
+ ruleName: "review-unanalysable-nesting",
2777
+ verdict: "review",
2778
+ reason: "This command nests more wrapped shell payloads than the policy engine will unwrap, so some of what it runs was not read.",
2779
+ path: ""
2780
+ };
2781
+ var ASSIGNMENT_HEADS = /* @__PURE__ */ new Set(["export", "declare", "local", "readonly", "typeset"]);
2782
+ function recordTopLevelAssignments(f) {
2783
+ const stmts = Array.isArray(f?.Stmts) ? f.Stmts : [];
2784
+ for (const stmt of stmts) recordTopLevelStmt(stmt);
2785
+ }
2786
+ function probeBinOp(src) {
2787
+ try {
2788
+ const cmd = syntax.NewParser().Parse(src, "probe")?.Stmts?.[0]?.Cmd;
2789
+ if (cmd && syntax.NodeType(cmd) === "BinaryCmd") return cmd.Op;
2790
+ } catch {
2791
+ }
2792
+ return null;
2793
+ }
2794
+ var AND_OP = probeBinOp("a && b");
2795
+ var OR_OP = probeBinOp("a || b");
2796
+ var AND_OR_OPS = new Set([AND_OP, OR_OP].filter((o) => o !== null));
2797
+ function recordTopLevelStmt(stmt) {
2798
+ if (!stmt || !stmt.Cmd) return false;
2799
+ const t = syntax.NodeType(stmt.Cmd);
2800
+ if (t === "BinaryCmd") {
2801
+ if (!AND_OR_OPS.has(stmt.Cmd.Op)) return false;
2802
+ if (!recordTopLevelStmt(stmt.Cmd.X)) return false;
2803
+ if (stmt.Cmd.Op === AND_OP) return recordTopLevelStmt(stmt.Cmd.Y);
2804
+ return true;
2805
+ }
2806
+ if (t !== "CallExpr" && t !== "DeclClause") return false;
2807
+ let at = 0;
2808
+ try {
2809
+ at = stmt.Pos().Offset();
2810
+ } catch {
2811
+ at = 0;
2812
+ }
2813
+ recordAssignments(stmt.Cmd, at);
2814
+ if (stmt.Negated) return false;
2815
+ if (t === "DeclClause") return ASSIGNMENT_HEADS.has(stmt.Cmd.Variant?.Value ?? "");
2816
+ return (stmt.Cmd.Args || []).length === 0 && (stmt.Cmd.Assigns || []).length > 0;
2817
+ }
2818
+ function recordAssignments(n, at) {
2819
+ if (!assignmentTable) return;
2820
+ const t = syntax.NodeType(n);
2821
+ let assigns = [];
2822
+ if (t === "CallExpr") {
2823
+ if ((n.Args || []).length > 0) return;
2824
+ assigns = n.Assigns || [];
2825
+ } else if (t === "DeclClause") {
2826
+ if (!ASSIGNMENT_HEADS.has(n.Variant?.Value ?? "")) return;
2827
+ assigns = (n.Args || []).filter((a) => syntax.NodeType(a) === "Assign");
2828
+ } else return;
2829
+ for (const a of assigns) {
2830
+ const name = a?.Name?.Value;
2831
+ if (!name || !a.Value || a.Append) continue;
2832
+ assignmentTable.set(name, { value: resolveWordLiteral(a.Value), at });
2833
+ }
2834
+ }
2835
+ function resolveTrivialSubst(part) {
2836
+ if (syntax.NodeType(part) !== "CmdSubst") return void 0;
2837
+ const stmts = part.Stmts || [];
2838
+ if (stmts.length !== 1) return void 0;
2839
+ const st = stmts[0];
2840
+ if ((st.Redirs || []).length > 0 || st.Negated || st.Background) return void 0;
2841
+ const cmd = st.Cmd;
2842
+ if (!cmd || syntax.NodeType(cmd) !== "CallExpr") return void 0;
2843
+ if ((cmd.Assigns || []).length > 0) return void 0;
2844
+ const words = (cmd.Args || []).map((a) => resolveWordLiteral(a));
2845
+ if (words.length === 0 || words.some((w) => w === null)) return void 0;
2846
+ const head = baseWord(words[0]);
2847
+ const rest = words.slice(1);
2848
+ if (head === "echo") {
2849
+ let i = 0;
2850
+ while (i < rest.length && /^-[neE]+$/.test(rest[i])) i++;
2851
+ return rest.slice(i).join(" ");
2852
+ }
2853
+ if (head === "printf") {
2854
+ if (rest.length !== 2) return void 0;
2855
+ if (!/^%s(\\n)?$/.test(rest[0])) return void 0;
2856
+ return rest[1];
2857
+ }
2858
+ return void 0;
2859
+ }
2860
+ function recordedExpansion(name) {
2861
+ if (!assignmentTable || !name) return void 0;
2862
+ const rec = assignmentTable.get(name);
2863
+ if (rec === void 0 || rec.at >= currentStmtOffset) return void 0;
2864
+ return rec.value;
2865
+ }
2866
+ function isPlainParam(p) {
2867
+ if (syntax.NodeType(p) !== "ParamExp") return false;
2868
+ return !(p.Excl || p.Length || p.Width || p.Index || p.Slice || p.Repl || p.Exp);
2869
+ }
2870
+ function expandPlainParam(p) {
2871
+ if (!assignmentTable) return void 0;
2872
+ if (!isPlainParam(p)) return void 0;
2873
+ const recorded = recordedExpansion(p.Param?.Value);
2874
+ if (recorded !== void 0) return recorded;
2875
+ return HOME_VARIABLES.has(p.Param?.Value) ? "~" : void 0;
2876
+ }
2697
2877
  function resolveWordLiteral(w) {
2698
2878
  const parts = w?.Parts || [];
2699
2879
  let s = "";
2700
2880
  for (const p of parts) {
2701
- const t = syntax.NodeType(p);
2702
- if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
2703
- else if (t === "SglQuoted") s += p.Value ?? "";
2704
- else if (t === "DblQuoted") {
2705
- const inner = p.Parts || [];
2706
- if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
2707
- s += inner.map((ip) => ip.Value ?? "").join("");
2708
- } else {
2709
- return null;
2710
- }
2881
+ const piece = resolvePart(p, false);
2882
+ if (piece === void 0 || piece === null) return null;
2883
+ s += piece;
2711
2884
  }
2712
2885
  return s;
2713
2886
  }
2887
+ function resolvePart(p, inQuotes) {
2888
+ const t = syntax.NodeType(p);
2889
+ if (t === "Lit") {
2890
+ const raw = p.Value ?? "";
2891
+ if (!inQuotes) return raw.replace(/\\(.)/g, "$1");
2892
+ return assignmentTable ? raw.replace(/\\([$`"\\])/g, "$1") : raw;
2893
+ }
2894
+ if (t === "SglQuoted") return p.Value ?? "";
2895
+ if (t === "ParamExp") return expandPlainParam(p);
2896
+ if (t === "CmdSubst") return assignmentTable ? resolveTrivialSubst(p) : void 0;
2897
+ if (t === "DblQuoted" && !inQuotes) {
2898
+ const inner = p.Parts || [];
2899
+ let out = "";
2900
+ for (const ip of inner) {
2901
+ const piece = resolvePart(ip, true);
2902
+ if (piece === void 0 || piece === null) return piece;
2903
+ out += piece;
2904
+ }
2905
+ return out;
2906
+ }
2907
+ return void 0;
2908
+ }
2714
2909
  function parseDestHost(token) {
2715
2910
  if (!token) return null;
2716
2911
  let t = token.trim();
@@ -2765,6 +2960,7 @@ function destTokensForBinary(binary, args) {
2765
2960
  case "ssh":
2766
2961
  return positionals.slice(0, 1);
2767
2962
  case "scp":
2963
+ case "rsync":
2768
2964
  return positionals.filter((p) => p.includes(":") || p.includes("@"));
2769
2965
  case "nc":
2770
2966
  case "ncat":
@@ -2965,17 +3161,35 @@ function analyzeFsOperationImpl(command, depth = 0) {
2965
3161
  const f = parseShared(command);
2966
3162
  if (f === PARSE_FAIL) return null;
2967
3163
  let result = null;
3164
+ const outerTable = assignmentTable;
3165
+ const outerOffset = currentStmtOffset;
3166
+ const outerSeen = seenPayloads;
3167
+ const outerBudget = payloadBudget;
3168
+ if (depth === 0) {
3169
+ seenPayloads = /* @__PURE__ */ new Set();
3170
+ payloadBudget = PAYLOAD_BUDGET;
3171
+ }
3172
+ assignmentTable = new Map(
3173
+ [...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
3174
+ );
3175
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
2968
3176
  try {
3177
+ recordTopLevelAssignments(f);
2969
3178
  syntax.Walk(f, (node) => {
2970
3179
  if (!node || result?.verdict === "block") return false;
2971
3180
  const n = node;
2972
3181
  const nodeType = syntax.NodeType(n);
2973
3182
  if (nodeType === "Stmt") {
3183
+ try {
3184
+ currentStmtOffset = n.Pos().Offset();
3185
+ } catch {
3186
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
3187
+ }
2974
3188
  result = stricter(result, jailedRedirectRead(n));
2975
3189
  return result?.verdict !== "block";
2976
3190
  }
2977
3191
  if (nodeType !== "CallExpr") return true;
2978
- const { name, flags, paths, words, args } = extractLiteralArgs(n);
3192
+ const { name, flags, paths, words } = extractLiteralArgs(n);
2979
3193
  if (!name) return true;
2980
3194
  if (name === "rm") {
2981
3195
  const flagStr = flags.join("").toLowerCase();
@@ -3004,9 +3218,30 @@ function analyzeFsOperationImpl(command, depth = 0) {
3004
3218
  }
3005
3219
  }
3006
3220
  }
3007
- if (depth < 1) {
3221
+ if (depth < 24 && name === "find") {
3222
+ for (const action of findActions(words, 0)) {
3223
+ const h = unwrapCommandHead(action);
3224
+ const inner = literalShellPayload(action.slice(h), baseWord(action[h]));
3225
+ if (inner === null) continue;
3226
+ const claim = claimPayload(inner);
3227
+ if (claim === "exhausted") {
3228
+ result = stricter(result, UNANALYSABLE_NESTING);
3229
+ continue;
3230
+ }
3231
+ if (claim === "seen") continue;
3232
+ const v = analyzeFsOperationImpl(inner, depth + 1);
3233
+ result = stricter(result, v);
3234
+ if (result?.verdict === "block") return false;
3235
+ }
3236
+ }
3237
+ if (depth < 24) {
3008
3238
  const payload = literalShellPayload(words, name);
3009
- if (payload !== null) {
3239
+ const claim = payload === null ? "seen" : claimPayload(payload);
3240
+ if (claim === "exhausted") {
3241
+ result = stricter(result, UNANALYSABLE_NESTING);
3242
+ return true;
3243
+ }
3244
+ if (payload !== null && claim === "ok") {
3010
3245
  const inner = analyzeFsOperationImpl(payload, depth + 1);
3011
3246
  if (inner) {
3012
3247
  result = inner;
@@ -3015,7 +3250,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
3015
3250
  return true;
3016
3251
  }
3017
3252
  }
3018
- const readPaths = FS_READ_TOOLS.has(name) ? [...readTargets(name, args, flags, words, 1), ...flagOperandFiles(name, words, 1)] : wrappedReadPaths(words, name);
3253
+ const readPaths = FS_READ_TOOLS.has(name) ? readerPaths(words, 0) : wrappedReadPaths(words, name);
3019
3254
  if (readPaths) {
3020
3255
  for (const p of readPaths) {
3021
3256
  result = stricter(result, matchSensitivePath2(p));
@@ -3030,6 +3265,11 @@ function analyzeFsOperationImpl(command, depth = 0) {
3030
3265
  return result;
3031
3266
  } catch {
3032
3267
  return null;
3268
+ } finally {
3269
+ assignmentTable = outerTable;
3270
+ currentStmtOffset = outerOffset;
3271
+ seenPayloads = outerSeen;
3272
+ if (depth === 0) payloadBudget = outerBudget;
3033
3273
  }
3034
3274
  }
3035
3275
  function stricter(a, b) {
@@ -3087,6 +3327,18 @@ function resolveCopyShape(words, h) {
3087
3327
  return null;
3088
3328
  }
3089
3329
  var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
3330
+ function findAction(words, k) {
3331
+ const end = words.findIndex((w, i) => i > k && (w === ";" || w === "+"));
3332
+ return words.slice(k + 1, end < 0 ? words.length : end);
3333
+ }
3334
+ function findActions(words, h) {
3335
+ const out = [];
3336
+ for (let i = h + 1; i < words.length; i++) {
3337
+ const w = words[i];
3338
+ if (w !== null && FIND_EXEC_FLAGS.has(w)) out.push(findAction(words, i));
3339
+ }
3340
+ return out;
3341
+ }
3090
3342
  function findStartPoints(words, h) {
3091
3343
  const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
3092
3344
  if (k < 0) return { k, starts: [] };
@@ -3102,8 +3354,13 @@ function copySourcePaths(words) {
3102
3354
  if (fi >= 0) {
3103
3355
  const { k, starts } = findStartPoints(words, fi);
3104
3356
  if (k < 0) return [];
3105
- const action = unwrapCommandHead(words.slice(k + 1));
3106
- return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
3357
+ const out = [];
3358
+ for (const action of findActions(words, fi)) {
3359
+ const h2 = unwrapCommandHead(action);
3360
+ if (!resolveCopyShape(action, h2)) continue;
3361
+ out.push(...starts, ...copySourcePaths(action));
3362
+ }
3363
+ return out;
3107
3364
  }
3108
3365
  if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
3109
3366
  const r = resolveCopyShape(words, h);
@@ -3290,6 +3547,15 @@ function flagEffect(token, shape, known) {
3290
3547
  }
3291
3548
  return NONE;
3292
3549
  }
3550
+ function readerPaths(words, h) {
3551
+ const head = baseWord(words[h]);
3552
+ const from = h + 1;
3553
+ const flags = words.slice(from).filter((w) => w !== null && w.startsWith("-"));
3554
+ return [
3555
+ ...readTargets(head, positionedArgs(words, from), flags, words, from),
3556
+ ...flagOperandFiles(head, words, from)
3557
+ ];
3558
+ }
3293
3559
  function readTargets(verb, args, flags, words = [], from = 1) {
3294
3560
  const shape = PATTERN_VERBS[verb];
3295
3561
  if (!shape) return args.map((a) => a.value);
@@ -3371,18 +3637,21 @@ function flagOperandFiles(verb, words, from) {
3371
3637
  function wrappedReadPaths(words, name) {
3372
3638
  if (name === "find") {
3373
3639
  const { k, starts } = findStartPoints(words, 0);
3374
- return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
3640
+ if (k < 0) return null;
3641
+ const paths = [];
3642
+ let reads = false;
3643
+ for (const action of findActions(words, 0)) {
3644
+ const h2 = unwrapCommandHead(action);
3645
+ if (!isReaderWord(action[h2] ?? null)) continue;
3646
+ reads = true;
3647
+ paths.push(...readerPaths(action, h2));
3648
+ }
3649
+ return reads ? [...starts, ...paths] : paths;
3375
3650
  }
3376
3651
  if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
3377
3652
  const h = unwrapCommandHead(words);
3378
3653
  if (h <= 0 || !isReaderWord(words[h] ?? null)) return null;
3379
- const head = baseWord(words[h]);
3380
- const rest = words.slice(h + 1);
3381
- const restFlags = rest.filter((w) => w !== null && w.startsWith("-"));
3382
- return [
3383
- ...readTargets(head, positionedArgs(words, h + 1), restFlags, words, h + 1),
3384
- ...flagOperandFiles(head, words, h + 1)
3385
- ];
3654
+ return readerPaths(words, h);
3386
3655
  }
3387
3656
  function literalShellPayload(words, name) {
3388
3657
  const h = COMMAND_WRAPPERS.has(name) || RUNNER_WRAPPERS.has(name) ? unwrapCommandHead(words) : 0;
package/dist/scan-ink.mjs CHANGED
@@ -1211,6 +1211,18 @@ var FS_OP_PRESCREEN_RE = new RegExp(
1211
1211
  // Dogfound 2026-09-22; pinned by verb-case-coverage.spec.ts.
1212
1212
  "i"
1213
1213
  );
1214
+ var currentStmtOffset = Number.MAX_SAFE_INTEGER;
1215
+ function probeBinOp(src) {
1216
+ try {
1217
+ const cmd = syntax.NewParser().Parse(src, "probe")?.Stmts?.[0]?.Cmd;
1218
+ if (cmd && syntax.NodeType(cmd) === "BinaryCmd") return cmd.Op;
1219
+ } catch {
1220
+ }
1221
+ return null;
1222
+ }
1223
+ var AND_OP = probeBinOp("a && b");
1224
+ var OR_OP = probeBinOp("a || b");
1225
+ var AND_OR_OPS = new Set([AND_OP, OR_OP].filter((o) => o !== null));
1214
1226
  function deriveRedirOp(sample) {
1215
1227
  try {
1216
1228
  const f = sharedParser.Parse(sample, "cmd");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.22.1",
3
+ "version": "2.23.0",
4
4
  "description": "IAM for your AI agents. Set what Claude Code, Codex, Gemini, Cursor and any MCP server are allowed to do, review risky actions before they run, and keep every action on the record.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",