@node9/proxy 2.33.1 → 2.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1210,6 +1210,120 @@ function expandPlainParam(p) {
1210
1210
  if (recorded !== void 0) return recorded;
1211
1211
  return HOME_VARIABLES.has(p.Param?.Value) ? "~" : void 0;
1212
1212
  }
1213
+ function functionBodies(f) {
1214
+ const out = /* @__PURE__ */ new Map();
1215
+ syntax.Walk(f, (node) => {
1216
+ const n = node;
1217
+ if (n && syntax.NodeType(n) === "FuncDecl" && n.Name?.Value && n.Body) {
1218
+ const list = out.get(n.Name.Value) ?? [];
1219
+ list.push(n.Body);
1220
+ out.set(n.Name.Value, list);
1221
+ }
1222
+ return true;
1223
+ });
1224
+ return out;
1225
+ }
1226
+ function rebindsPositionals(body) {
1227
+ let found = false;
1228
+ syntax.Walk(body, (node) => {
1229
+ const n = node;
1230
+ if (found || !n) return false;
1231
+ if (syntax.NodeType(n) === "CallExpr") {
1232
+ const words = (n.Args || []).map((a) => resolveWordLiteral(a));
1233
+ if (words[0] === "shift") found = true;
1234
+ else if (words[0] === "set" && words.slice(1).some((w) => w === null || w === "--" || !/^[-+]/.test(w)))
1235
+ found = true;
1236
+ }
1237
+ return !found;
1238
+ });
1239
+ return found;
1240
+ }
1241
+ function tableBindings() {
1242
+ if (!assignmentTable) return "";
1243
+ const out = [];
1244
+ for (const [name, rec] of assignmentTable)
1245
+ if (rec.at < currentStmtOffset) out.push(`${name}=${rec.value ?? "\0"}`);
1246
+ return out.sort().join("");
1247
+ }
1248
+ function walkWithCalls(f, visit2) {
1249
+ const bodies = functionBodies(f);
1250
+ if (bodies.size === 0) {
1251
+ syntax.Walk(f, visit2);
1252
+ return;
1253
+ }
1254
+ const seenCalls = /* @__PURE__ */ new Set();
1255
+ const inline = (call) => {
1256
+ const words = (call.Args || []).map((a) => resolveWordLiteral(a));
1257
+ const list = words[0] ? bodies.get(words[0]) : void 0;
1258
+ if (!list || callDepth >= MAX_CALL_DEPTH) return;
1259
+ const args = words.slice(1);
1260
+ const key = `${words[0]}\0${JSON.stringify(args)}\0${tableBindings()}`;
1261
+ if (seenCalls.has(key)) return;
1262
+ seenCalls.add(key);
1263
+ const outerTable = assignmentTable;
1264
+ const outerOffset = currentStmtOffset;
1265
+ for (const body of list) {
1266
+ if (callWalks >= MAX_CALL_WALKS) return;
1267
+ callWalks++;
1268
+ const table = /* @__PURE__ */ new Map();
1269
+ for (const [name, rec] of outerTable ?? [])
1270
+ if (rec.at < outerOffset) table.set(name, { value: rec.value, at: -1 });
1271
+ for (const name of ["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*", "#"])
1272
+ table.delete(name);
1273
+ if (!rebindsPositionals(body)) {
1274
+ args.slice(0, 9).forEach((value, i) => table.set(String(i + 1), { value, at: -1 }));
1275
+ if (args.length === 1) {
1276
+ table.set("@", { value: args[0], at: -1 });
1277
+ table.set("*", { value: args[0], at: -1 });
1278
+ }
1279
+ table.set("#", { value: String(args.length), at: -1 });
1280
+ }
1281
+ assignmentTable = table;
1282
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1283
+ callDepth++;
1284
+ try {
1285
+ if (body.Cmd && syntax.NodeType(body.Cmd) === "Block") recordTopLevelAssignments(body.Cmd);
1286
+ syntax.Walk(body, step);
1287
+ } finally {
1288
+ callDepth--;
1289
+ assignmentTable = outerTable;
1290
+ currentStmtOffset = outerOffset;
1291
+ }
1292
+ }
1293
+ };
1294
+ const step = (node) => {
1295
+ if (!node) return false;
1296
+ const go = visit2(node);
1297
+ if (go !== false && syntax.NodeType(node) === "CallExpr") inline(node);
1298
+ return go;
1299
+ };
1300
+ syntax.Walk(f, step);
1301
+ }
1302
+ function withAssignments(f, body) {
1303
+ const outerTable = assignmentTable;
1304
+ const outerOffset = currentStmtOffset;
1305
+ const outerWalks = callWalks;
1306
+ callWalks = 0;
1307
+ assignmentTable = new Map(
1308
+ [...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
1309
+ );
1310
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1311
+ try {
1312
+ recordTopLevelAssignments(f);
1313
+ return body();
1314
+ } finally {
1315
+ assignmentTable = outerTable;
1316
+ currentStmtOffset = outerOffset;
1317
+ callWalks = outerWalks;
1318
+ }
1319
+ }
1320
+ function trackStatement(n) {
1321
+ try {
1322
+ currentStmtOffset = n.Pos().Offset();
1323
+ } catch {
1324
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1325
+ }
1326
+ }
1213
1327
  function resolveWordLiteral(w) {
1214
1328
  const parts = w?.Parts || [];
1215
1329
  let s = "";
@@ -1330,27 +1444,31 @@ function extractShellDestinations(command) {
1330
1444
  const out = [];
1331
1445
  const seen = /* @__PURE__ */ new Set();
1332
1446
  try {
1333
- syntax.Walk(f, (node) => {
1334
- if (!node) return false;
1335
- const n = node;
1336
- if (syntax.NodeType(n) !== "CallExpr") return true;
1337
- const callArgs = n.Args || [];
1338
- if (callArgs.length === 0) return true;
1339
- const words = callArgs.map((a) => resolveWordLiteral(a));
1340
- const at = unwrapCommandHead(words);
1341
- const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
1342
- if (!NET_BINARIES.has(name)) return true;
1343
- const rest = words.slice(at + 1);
1344
- for (const raw of destTokensForBinary(name, rest)) {
1345
- const host2 = parseDestHost(raw);
1346
- if (!host2) continue;
1347
- const key = `${name}:${host2}`;
1348
- if (seen.has(key)) continue;
1349
- seen.add(key);
1350
- out.push({ host: host2, binary: name, raw });
1351
- }
1352
- return true;
1353
- });
1447
+ withAssignments(
1448
+ f,
1449
+ () => walkWithCalls(f, (node) => {
1450
+ if (!node) return false;
1451
+ const n = node;
1452
+ if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1453
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1454
+ const callArgs = n.Args || [];
1455
+ if (callArgs.length === 0) return true;
1456
+ const words = callArgs.map((a) => resolveWordLiteral(a));
1457
+ const at = unwrapCommandHead(words);
1458
+ const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
1459
+ if (!NET_BINARIES.has(name)) return true;
1460
+ const rest = words.slice(at + 1);
1461
+ for (const raw of destTokensForBinary(name, rest)) {
1462
+ const host2 = parseDestHost(raw);
1463
+ if (!host2) continue;
1464
+ const key = `${name}:${host2}`;
1465
+ if (seen.has(key)) continue;
1466
+ seen.add(key);
1467
+ out.push({ host: host2, binary: name, raw });
1468
+ }
1469
+ return true;
1470
+ })
1471
+ );
1354
1472
  } catch {
1355
1473
  return out;
1356
1474
  }
@@ -1362,39 +1480,45 @@ function extractShellDestTokens(command) {
1362
1480
  const out = [];
1363
1481
  const seen = /* @__PURE__ */ new Set();
1364
1482
  try {
1365
- syntax.Walk(f, (node) => {
1366
- if (!node) return false;
1367
- const n = node;
1368
- if (syntax.NodeType(n) !== "CallExpr") return true;
1369
- const callArgs = n.Args || [];
1370
- if (callArgs.length === 0) return true;
1371
- const name = (resolveWordLiteral(callArgs[0]) || "").toLowerCase();
1372
- if (!NET_BINARIES.has(name)) return true;
1373
- const rest = callArgs.slice(1).map((a) => resolveWordLiteral(a));
1374
- for (const raw of destTokensForBinary(name, rest)) {
1375
- if (!raw) continue;
1376
- let tok = raw.trim();
1377
- const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(tok);
1378
- const hasScheme = scheme !== null;
1379
- if (hasScheme) tok = tok.slice(scheme[0].length);
1380
- tok = tok.split(/[/?#]/)[0];
1381
- const at = tok.lastIndexOf("@");
1382
- if (at >= 0) tok = tok.slice(at + 1);
1383
- if (tok.startsWith("[")) {
1384
- const close = tok.indexOf("]");
1385
- if (close > 0) tok = tok.slice(0, close + 1);
1386
- } else {
1387
- tok = tok.split(":")[0];
1483
+ withAssignments(
1484
+ f,
1485
+ () => walkWithCalls(f, (node) => {
1486
+ if (!node) return false;
1487
+ const n = node;
1488
+ if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1489
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1490
+ const callArgs = n.Args || [];
1491
+ if (callArgs.length === 0) return true;
1492
+ const words = callArgs.map((a) => resolveWordLiteral(a));
1493
+ const head = unwrapCommandHead(words);
1494
+ const name = (words[head] || "").toLowerCase().split("/").pop() ?? "";
1495
+ if (!NET_BINARIES.has(name)) return true;
1496
+ const rest = words.slice(head + 1);
1497
+ for (const raw of destTokensForBinary(name, rest)) {
1498
+ if (!raw) continue;
1499
+ let tok = raw.trim();
1500
+ const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(tok);
1501
+ const hasScheme = scheme !== null;
1502
+ if (hasScheme) tok = tok.slice(scheme[0].length);
1503
+ tok = tok.split(/[/?#]/)[0];
1504
+ const at = tok.lastIndexOf("@");
1505
+ if (at >= 0) tok = tok.slice(at + 1);
1506
+ if (tok.startsWith("[")) {
1507
+ const close = tok.indexOf("]");
1508
+ if (close > 0) tok = tok.slice(0, close + 1);
1509
+ } else {
1510
+ tok = tok.split(":")[0];
1511
+ }
1512
+ if (!tok) continue;
1513
+ if (!hasScheme && /^\d+$/.test(tok) && Number(tok) < 16777216) continue;
1514
+ const key = `${name}:${tok}`;
1515
+ if (seen.has(key)) continue;
1516
+ seen.add(key);
1517
+ out.push({ token: tok, binary: name });
1388
1518
  }
1389
- if (!tok) continue;
1390
- if (!hasScheme && /^\d+$/.test(tok) && Number(tok) < 16777216) continue;
1391
- const key = `${name}:${tok}`;
1392
- if (seen.has(key)) continue;
1393
- seen.add(key);
1394
- out.push({ token: tok, binary: name });
1395
- }
1396
- return true;
1397
- });
1519
+ return true;
1520
+ })
1521
+ );
1398
1522
  } catch {
1399
1523
  return out;
1400
1524
  }
@@ -1513,6 +1637,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1513
1637
  if (depth === 0) {
1514
1638
  seenPayloads = /* @__PURE__ */ new Set();
1515
1639
  payloadBudget = PAYLOAD_BUDGET;
1640
+ if (callDepth === 0) callWalks = 0;
1516
1641
  }
1517
1642
  assignmentTable = new Map(
1518
1643
  [...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
@@ -1520,7 +1645,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1520
1645
  currentStmtOffset = Number.MAX_SAFE_INTEGER;
1521
1646
  try {
1522
1647
  recordTopLevelAssignments(f);
1523
- syntax.Walk(f, (node) => {
1648
+ walkWithCalls(f, (node) => {
1524
1649
  if (!node || result?.verdict === "block") return false;
1525
1650
  const n = node;
1526
1651
  const nodeType = syntax.NodeType(n);
@@ -2309,6 +2434,19 @@ function runnablePrefix(command) {
2309
2434
  }
2310
2435
  return null;
2311
2436
  }
2437
+ function uploadedFile(tool, flag, value) {
2438
+ const v = value.value;
2439
+ const file2 = (f) => f && f !== "-" && f !== "." ? { value: f } : null;
2440
+ if (tool === "curl") {
2441
+ if (["-d", "--data", "--data-ascii", "--data-binary", "--json"].includes(flag))
2442
+ return v.startsWith("@") ? file2(v.slice(1)) : null;
2443
+ if (flag === "--data-urlencode") return file2(/^[^=@]*@(.+)$/.exec(v)?.[1]);
2444
+ if (flag === "-F" || flag === "--form") return file2(/^[^=]*=[@<]([^;]+)/.exec(v)?.[1]);
2445
+ if (flag === "-T" || flag === "--upload-file") return file2(v);
2446
+ }
2447
+ if (tool === "wget" && ["--post-file", "--body-file"].includes(flag)) return file2(v);
2448
+ return null;
2449
+ }
2312
2450
  function merge(a, b) {
2313
2451
  a.cwd = unique([...a.cwd, ...b.cwd]);
2314
2452
  for (const key of /* @__PURE__ */ new Set([...a.vars.keys(), ...b.vars.keys()])) {
@@ -2382,6 +2520,9 @@ function wordValues(word, state) {
2382
2520
  function collectShellFileAccesses(command, context = {}) {
2383
2521
  const out = [];
2384
2522
  let payloads = 256;
2523
+ const functions = /* @__PURE__ */ new Map();
2524
+ const seenCalls = /* @__PURE__ */ new Set();
2525
+ let callWalks2 = MAX_CALL_WALKS2;
2385
2526
  const initial = {
2386
2527
  cwd: [context.cwd ?? ""],
2387
2528
  vars: /* @__PURE__ */ new Map([["HOME", [context.homeDirs?.[0] ?? "~"]]])
@@ -2639,8 +2780,98 @@ function collectShellFileAccesses(command, context = {}) {
2639
2780
  }
2640
2781
  return;
2641
2782
  }
2783
+ if (name === "wget" || name === "curl" || name === "aria2c") {
2784
+ downloadOutputs(name, args, state);
2785
+ return;
2786
+ }
2787
+ if (["http", "https", "xh", "xhs"].includes(name)) {
2788
+ for (const token of args) {
2789
+ if (!token || token.dynamic || /^[a-z][a-z0-9+.-]*:\/\//i.test(token.value)) continue;
2790
+ const m = /^(?:@|[^=:@\s]+@|[^=:@\s]*=@)(.+)$/.exec(token.value);
2791
+ if (m && m[1] !== "-") add("read", { value: m[1] }, state, { upload: true });
2792
+ }
2793
+ return;
2794
+ }
2795
+ if (name === "scp" || name === "rsync") {
2796
+ const operands2 = strings.filter((s) => s && !s.startsWith("-"));
2797
+ const remote = operands2.slice(1).some((o) => /^[^/\s]+:/.test(o));
2798
+ for (const p of copySourcePaths(words))
2799
+ add("read", locate(p), state, remote ? { upload: true } : {});
2800
+ return;
2801
+ }
2642
2802
  for (const p of copySourcePaths(words)) add("read", locate(p), state);
2643
2803
  }
2804
+ function downloadOutputs(name, args, state) {
2805
+ const valueShort = DOWNLOAD_SHORT_VALUE[name];
2806
+ const outputs = [];
2807
+ const urls = [];
2808
+ let dir2 = null;
2809
+ let remoteName = false;
2810
+ let document = false;
2811
+ let serverNamed = false;
2812
+ const take = (flag, value) => {
2813
+ if (!value) return;
2814
+ const sent = uploadedFile(name, flag, value);
2815
+ if (sent) add("read", sent, state, { upload: true });
2816
+ if (DOWNLOAD_OUTPUT_FLAGS[name].includes(flag)) {
2817
+ if (["-O", "--output-document", "--out"].includes(flag) || name === "aria2c" && flag === "-o")
2818
+ document = true;
2819
+ if (value.value !== "-" || value.pattern || value.dynamic) outputs.push(value);
2820
+ } else if (DOWNLOAD_DIR_FLAGS[name].includes(flag)) dir2 = value;
2821
+ else if (name === "wget" && (flag === "-i" || flag === "--input-file")) serverNamed = true;
2822
+ };
2823
+ for (let i = 0; i < args.length; i++) {
2824
+ const token = args[i];
2825
+ if (!token) continue;
2826
+ const v = token.value;
2827
+ if (v.startsWith("--")) {
2828
+ const eq = v.indexOf("=");
2829
+ const flag = eq < 0 ? v : v.slice(0, eq);
2830
+ if (["--remote-name", "--remote-name-all"].includes(flag)) remoteName = true;
2831
+ if (["--remote-header-name", "--recursive", "--mirror", "--page-requisites"].includes(flag))
2832
+ serverNamed = true;
2833
+ if (DOWNLOAD_LONG_VALUE[name].includes(flag))
2834
+ take(flag, eq < 0 ? args[++i] ?? null : { ...token, value: v.slice(eq + 1) });
2835
+ continue;
2836
+ }
2837
+ if (v.startsWith("-") && v.length > 1 && !token.dynamic) {
2838
+ for (let j = 1; j < v.length; j++) {
2839
+ const c = v[j];
2840
+ if (name === "wget" && c === "n") {
2841
+ j++;
2842
+ continue;
2843
+ }
2844
+ if (name === "curl" && c === "O") remoteName = true;
2845
+ if (name === "curl" && c === "J") serverNamed = true;
2846
+ if (name === "wget" && (c === "r" || c === "m" || c === "p")) serverNamed = true;
2847
+ if (valueShort.includes(c)) {
2848
+ const rest = v.slice(j + 1);
2849
+ take(`-${c}`, rest ? { value: rest } : args[++i] ?? null);
2850
+ break;
2851
+ }
2852
+ }
2853
+ continue;
2854
+ }
2855
+ if (/^(?:https?|ftps?):\/\//i.test(v)) urls.push(v);
2856
+ }
2857
+ for (const out2 of outputs) {
2858
+ const spelled = name === "aria2c" && dir2?.value && !out2.value.startsWith("/") ? { ...out2, value: `${dir2.value.replace(/\/+$/, "")}/${out2.value}` } : out2;
2859
+ if (name === "curl" && /#\d/.test(spelled.value))
2860
+ add("write", { value: "", pattern: spelled.value.replace(/#\d+/g, "*"), dot: true }, state);
2861
+ else add("write", spelled, state);
2862
+ }
2863
+ const remote = name === "curl" ? remoteName || serverNamed : !document || serverNamed;
2864
+ if (!remote) return;
2865
+ const into = dir2?.value ? dir2.value.replace(/\/+$/, "") : ".";
2866
+ if (serverNamed || urls.length === 0) {
2867
+ add("write", { value: "", pattern: `${quoteGlob(into)}/*`, dot: true }, state);
2868
+ return;
2869
+ }
2870
+ for (const url of urls) {
2871
+ const file2 = url.replace(/[?#].*$/, "").replace(/^[a-z]+:\/\/[^/]*/i, "").split("/").pop();
2872
+ add("write", { value: `${into}/${file2 || "index.html"}` }, state);
2873
+ }
2874
+ }
2644
2875
  function findActions2(tokens, words, head, state, nested, depth) {
2645
2876
  const starts = findStartPoints(words, head).starts;
2646
2877
  const roots = (starts.length ? starts : ["."]).slice(0, MAX_ALTERNATIVES);
@@ -2712,7 +2943,35 @@ function collectShellFileAccesses(command, context = {}) {
2712
2943
  const words = wordValues(node, state);
2713
2944
  alternatives = alternatives.flatMap((prefix) => (words.length ? words : [null]).map((w) => [...prefix, w])).slice(0, MAX_ALTERNATIVES);
2714
2945
  }
2715
- for (const tokens of alternatives) fromWords(tokens, state, nested, depth);
2946
+ for (const tokens of alternatives) {
2947
+ fromWords(tokens, state, nested, depth);
2948
+ inlineCall(tokens, state, depth);
2949
+ }
2950
+ }
2951
+ function inlineCall(tokens, state, depth) {
2952
+ const head = tokens[0];
2953
+ if (!head || head.dynamic || head.pattern) return;
2954
+ const bodies = functions.get(head.value);
2955
+ if (!bodies || (state.callDepth ?? 0) >= MAX_CALL_DEPTH2) return;
2956
+ const args = tokens.slice(1).map((a) => a && !a.dynamic ? a.value : null);
2957
+ const vars = JSON.stringify([...state.vars].sort(([a], [b]) => a < b ? -1 : 1));
2958
+ const key = `${head.value}\0${JSON.stringify(args)}\0${state.cwd.join("")}\0${vars}`;
2959
+ if (seenCalls.has(key)) return;
2960
+ seenCalls.add(key);
2961
+ for (const body of bodies) {
2962
+ if (callWalks2-- <= 0) return;
2963
+ const local = clone(state);
2964
+ local.callDepth = (state.callDepth ?? 0) + 1;
2965
+ for (const name of POSITIONAL) local.vars.delete(name);
2966
+ if (!rebindsPositionals(body)) {
2967
+ args.slice(0, 9).forEach((v, i) => local.vars.set(String(i + 1), v === null ? null : [v]));
2968
+ const all = args.every((v) => v !== null) && args.length ? unique(args) : null;
2969
+ local.vars.set("@", all);
2970
+ local.vars.set("*", all);
2971
+ local.vars.set("#", [String(args.length)]);
2972
+ }
2973
+ statement(body, local, depth + 1);
2974
+ }
2716
2975
  }
2717
2976
  function readsScriptFromStdin(stmt) {
2718
2977
  const cmd = stmt?.Cmd;
@@ -2749,11 +3008,13 @@ function collectShellFileAccesses(command, context = {}) {
2749
3008
  if (!stmt || depth > MAX_DEPTH) return;
2750
3009
  state.onSuccess = void 0;
2751
3010
  if (stmt.Background) state = clone(state);
3011
+ const headWord = stmt.Cmd && syntax2.NodeType(stmt.Cmd) === "CallExpr" ? base(resolveWordLiteral(stmt.Cmd.Args?.[0])) : "";
3012
+ const sends = SENDING_STDIN.has(headWord);
2752
3013
  for (const redir of stmt.Redirs ?? []) {
2753
3014
  substitutions(redir.Word, state, depth);
2754
3015
  for (const word of wordValues(redir.Word, state)) {
2755
3016
  if (WRITES.has(redir.Op)) add("write", word, state);
2756
- if (READS.has(redir.Op)) add("read", word, state);
3017
+ if (READS.has(redir.Op)) add("read", word, state, sends ? { upload: true } : {});
2757
3018
  if (redir.Op === DUP_OUT && word.value && !/^\d+$/.test(word.value))
2758
3019
  add("write", word, state);
2759
3020
  }
@@ -2862,7 +3123,10 @@ function collectShellFileAccesses(command, context = {}) {
2862
3123
  }
2863
3124
  const parsed = parseShared(command);
2864
3125
  const program2 = typeof parsed !== "symbol" ? parsed : runnablePrefix(command);
2865
- if (program2) statements(program2.Stmts ?? [], initial, 0);
3126
+ if (program2) {
3127
+ for (const [name, bodies] of functionBodies(program2)) functions.set(name, bodies);
3128
+ statements(program2.Stmts ?? [], initial, 0);
3129
+ }
2866
3130
  return out;
2867
3131
  }
2868
3132
  function isAgentInstructionPath(absPath, root) {
@@ -2978,6 +3242,33 @@ function hits(access, paths, directories, context) {
2978
3242
  }
2979
3243
  return false;
2980
3244
  }
3245
+ function hitsSystemDir(access, context, dirs = SYSTEM_DIRS) {
3246
+ const home = context.homeDirs?.[0] ?? "~";
3247
+ const candidates2 = access.candidates ?? [
3248
+ normalizeFilePath(access.path, access.cwd ?? context.cwd, home)
3249
+ ];
3250
+ for (const p of candidates2) {
3251
+ if (dirs.some((d) => within(p, d)) && !exempt(p)) return true;
3252
+ if (access.recursive && dirs.some((d) => within(d, p))) return true;
3253
+ }
3254
+ if (!access.glob) return false;
3255
+ const pattern = normalizeFilePath(access.glob, access.cwd ?? context.cwd, home);
3256
+ const magic = pattern.search(/[*?[{]/);
3257
+ const literal = magic < 0 ? pattern : pattern.slice(0, pattern.lastIndexOf("/", magic) + 1);
3258
+ if (literal && exempt(literal.replace(/\/$/, "") || "/")) return false;
3259
+ const options = { dot: access.dotGlob === true, nonegate: true };
3260
+ const components = pattern.split("/");
3261
+ for (const root of dirs) {
3262
+ const count = root.split("/").length;
3263
+ if (components.length > count && picomatch(components.slice(0, count).join("/"), options)(root))
3264
+ return true;
3265
+ if (access.recursive && picomatch(pattern, options)(root)) return true;
3266
+ }
3267
+ return false;
3268
+ }
3269
+ function uploadDirs(context) {
3270
+ return context.homeDirs?.[0] === "/root" ? UPLOAD_SYSTEM_DIRS.filter((d) => d !== "/root") : UPLOAD_SYSTEM_DIRS;
3271
+ }
2981
3272
  function evaluateProtectedFileAccess(accesses, context, settings) {
2982
3273
  const homes = [.../* @__PURE__ */ new Set([...context.homeDirs ?? ["~"], "/root"])];
2983
3274
  const startup = homes.flatMap(
@@ -2999,6 +3290,14 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
2999
3290
  "files.shell-startup",
3000
3291
  "review-shell-startup-mutation",
3001
3292
  "Changing shell startup files requires approval"
3293
+ ] : mutation && hitsSystemDir(access, context) ? [
3294
+ "files.system-dirs",
3295
+ "review-system-dir-mutation",
3296
+ "Changing files in a system directory requires approval"
3297
+ ] : !mutation && access.upload && hitsSystemDir(access, context, uploadDirs(context)) ? [
3298
+ "files.system-dirs",
3299
+ "review-system-file-upload",
3300
+ "Uploading a system file requires approval"
3002
3301
  ] : !mutation && hits(access, PASSWORD_HASH_FILES, [], context) ? [
3003
3302
  "files.password-hashes",
3004
3303
  "review-password-hash-read",
@@ -3326,16 +3625,21 @@ function looksGenerated(value, minLength, minEntropy, lettersOnlyAllowed = false
3326
3625
  if (EMAIL_VALUE_RE.test(value)) return false;
3327
3626
  if (looksLikeJoinedWords(value) || hasSequentialRun(value)) return false;
3328
3627
  if (PATH_VALUE_RE.test(value) || DOTTED_REFERENCE_RE.test(value)) return false;
3628
+ if (CONCATENATION_RE.test(value)) return false;
3329
3629
  if (rejectedByStructuredValidator(value)) return false;
3330
3630
  return shannonEntropy(value) >= minEntropy;
3331
3631
  }
3332
3632
  function nameHoldsCredential(name) {
3333
3633
  return !FIXTURE_NAME_RE.test(name) && !DERIVATIVE_NAME_RE.test(name);
3334
3634
  }
3635
+ function isOtpDocSecret(value) {
3636
+ return OTP_DOC_SECRETS.has(value.replace(/=+$/, "").toUpperCase());
3637
+ }
3335
3638
  function validateQueryCredential(raw) {
3336
3639
  const value = capturedValue(raw, QUERY_CREDENTIAL_VALUE_RE);
3337
3640
  if (value === null) return false;
3338
- if (OTP_SECRET_RE.test(value)) return looksGenerated(value, 16, 3, true);
3641
+ if (OTP_SECRET_RE.test(value))
3642
+ return !isOtpDocSecret(value) && looksGenerated(value, 16, 3, true);
3339
3643
  if (!/[0-9]/.test(value) || !/[A-Za-z]/.test(value)) return false;
3340
3644
  return looksGenerated(value, 12, 3);
3341
3645
  }
@@ -3349,13 +3653,40 @@ function keywordEndsAPart(name) {
3349
3653
  }
3350
3654
  function validateAssignedSecret(raw, before = "") {
3351
3655
  const value = capturedValue(raw, ASSIGNED_SECRET_VALUE_RE);
3352
- const name = capturedValue(raw, ASSIGNED_SECRET_NAME_RE);
3656
+ const name = capturedValue(raw, ASSIGNED_SECRET_NAME_RE)?.replace(/^_+/, "") ?? null;
3353
3657
  if (value === null || name === null) return false;
3354
- if (!keywordEndsAPart(name) || ARN_CONTEXT_RE.test(before)) return false;
3355
- const prefix = before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "";
3658
+ if (!keywordEndsAPart(name)) return false;
3659
+ if (ARN_CONTEXT_RE.test(before)) return false;
3660
+ const prefix = (before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "") + (raw.startsWith("_") ? "_" : "");
3356
3661
  if (!nameHoldsCredential(prefix + name)) return false;
3357
3662
  return looksGenerated(value, 12, 3.5);
3358
3663
  }
3664
+ function validateBasicCredential(raw) {
3665
+ const b64 = capturedValue(raw, BASIC_VALUE_RE);
3666
+ if (b64 === null) return false;
3667
+ const bare = b64.replace(/=+$/, "");
3668
+ if (bare.length % 4 === 1) return false;
3669
+ const bytes = Buffer.from(b64, "base64");
3670
+ if (bytes.toString("base64").replace(/=+$/, "") !== bare) return false;
3671
+ const text = bytes.toString("utf8");
3672
+ if (!Buffer.from(text, "utf8").equals(bytes)) return false;
3673
+ for (let i = 0; i < text.length; i++) {
3674
+ const c = text.charCodeAt(i);
3675
+ if (c < 32 || c === 127) return false;
3676
+ }
3677
+ const pair = BASIC_PAIR_RE.exec(text);
3678
+ if (!pair) return false;
3679
+ const password = pair[2];
3680
+ if (!/[0-9]/.test(password) || !/[A-Za-z]/.test(password)) return false;
3681
+ const lower2 = password.toLowerCase();
3682
+ if (DLP_STOPWORDS.some((sw) => lower2.includes(sw)) || hasSequentialRun(password)) return false;
3683
+ return shannonEntropy(password) >= 3;
3684
+ }
3685
+ function validateOtpSecret(raw) {
3686
+ const secret = capturedValue(raw, OTP_CAPTURE_RE);
3687
+ if (secret === null || isOtpDocSecret(secret)) return false;
3688
+ return looksGenerated(secret.replace(/=+$/, ""), 16, 3, true);
3689
+ }
3359
3690
  function matchSensitivePath2(resolvedPath, originalPath, options = {}) {
3360
3691
  const normalised = resolvedPath.replace(/\\/g, "/");
3361
3692
  if (!options.systemAccessHandled && isLegacySystemPath(normalised))
@@ -3664,6 +3995,156 @@ function parsePySpec(raw) {
3664
3995
  const exact = PY_EXACT_RE.exec(m[2].trim());
3665
3996
  return { name, version: exact ? exact[1] : void 0 };
3666
3997
  }
3998
+ function exactVersion(v) {
3999
+ if (!v) return void 0;
4000
+ const m = EXACT_VERSION_RE.exec(v.trim());
4001
+ return m ? m[1] : void 0;
4002
+ }
4003
+ function push(out, ecosystem, manager, name, version2, raw) {
4004
+ out.push({ ecosystem, manager, name, ...version2 ? { version: version2 } : {}, raw });
4005
+ }
4006
+ function gemInstall(words, from, out) {
4007
+ const dash = words.indexOf("--", from);
4008
+ const own = dash < 0 ? words : words.slice(0, dash);
4009
+ const pinned = exactVersion(flagValues(own, from, ["-v", "--version"])[0]);
4010
+ for (const raw of operands(own, from, GEM_VALUE_FLAGS)) {
4011
+ if (NON_REGISTRY_RE.test(raw) || /\.gem$/i.test(raw)) continue;
4012
+ const colon = raw.indexOf(":");
4013
+ const name = colon < 0 ? raw : raw.slice(0, colon);
4014
+ if (!GEM_NAME_RE.test(name)) continue;
4015
+ push(
4016
+ out,
4017
+ "RubyGems",
4018
+ "gem",
4019
+ name,
4020
+ colon < 0 ? pinned : exactVersion(raw.slice(colon + 1)),
4021
+ raw
4022
+ );
4023
+ }
4024
+ }
4025
+ function cargoCrates(words, from, manager, out) {
4026
+ const flags = manager === "cargo add" ? CARGO_ADD_VALUE_FLAGS : CARGO_INSTALL_VALUE_FLAGS;
4027
+ if (flagValues(words, from, ["--git", "--path", "--registry", "--index"]).length > 0) return;
4028
+ if (words.some((w) => w === "--git" || w === "--path")) return;
4029
+ const flagVersion = manager === "cargo install" ? exactVersion(flagValues(words, from, ["--version", "--vers"])[0]) : void 0;
4030
+ for (const raw of operands(words, from, flags)) {
4031
+ const at = raw.indexOf("@");
4032
+ const name = at < 0 ? raw : raw.slice(0, at);
4033
+ if (!CRATE_NAME_RE.test(name)) continue;
4034
+ push(
4035
+ out,
4036
+ "crates.io",
4037
+ manager,
4038
+ name,
4039
+ at < 0 ? flagVersion : exactVersion(raw.slice(at + 1)),
4040
+ raw
4041
+ );
4042
+ }
4043
+ }
4044
+ function goModules(words, from, manager, out) {
4045
+ let specs = operands(words, from, GO_VALUE_FLAGS);
4046
+ if (manager === "go run") specs = specs.slice(0, 1).filter((s) => s.includes("@"));
4047
+ for (const raw of specs) {
4048
+ const at = raw.indexOf("@");
4049
+ const pathPart = (at < 0 ? raw : raw.slice(0, at)).replace(/\/\.\.\.$/, "");
4050
+ const ver = at < 0 ? void 0 : raw.slice(at + 1);
4051
+ if (ver === "none") continue;
4052
+ if (!GO_PATH_RE.test(pathPart) || /\.go$/i.test(pathPart)) continue;
4053
+ const exact = ver && /^v\d/.test(ver) ? exactVersion(ver) : void 0;
4054
+ push(out, "Go", manager, pathPart, exact, raw);
4055
+ }
4056
+ }
4057
+ function dotnetAdd(words, from, out) {
4058
+ let k = -1;
4059
+ for (let i = from; i < Math.min(words.length, from + 2); i++) {
4060
+ if (lower(words[i]) === "package") {
4061
+ k = i;
4062
+ break;
4063
+ }
4064
+ }
4065
+ if (k < 0) return;
4066
+ const name = operands(words, k + 1, DOTNET_ADD_VALUE_FLAGS)[0];
4067
+ if (!name || !NUGET_NAME_RE.test(name)) return;
4068
+ const version2 = exactVersion(flagValues(words, k + 1, ["-v", "--version"])[0]);
4069
+ push(out, "NuGet", "dotnet add package", name, version2, name);
4070
+ }
4071
+ function dotnetTool(words, from, out) {
4072
+ const name = operands(words, from, DOTNET_TOOL_VALUE_FLAGS)[0];
4073
+ if (!name || !NUGET_NAME_RE.test(name)) return;
4074
+ const version2 = exactVersion(flagValues(words, from, ["--version"])[0]);
4075
+ push(out, "NuGet", "dotnet tool", name, version2, name);
4076
+ }
4077
+ function nugetInstall(words, from, out) {
4078
+ const lowered = words.map((w) => w !== null && w.startsWith("-") ? w.toLowerCase() : w);
4079
+ const name = operands(lowered, from, NUGET_VALUE_FLAGS)[0];
4080
+ if (!name || !NUGET_NAME_RE.test(name) || /\.config$/i.test(name)) return;
4081
+ const version2 = exactVersion(flagValues(lowered, from, ["-version"])[0]);
4082
+ push(out, "NuGet", "nuget install", name, version2, name);
4083
+ }
4084
+ function mavenGet(words, from, out) {
4085
+ const goal = words.slice(from).some(
4086
+ (w) => w !== null && /(?:^|:)dependency:get$|^org\.apache\.maven\.plugins:maven-dependency-plugin:(?:[\w.-]+:)?get$/.test(
4087
+ w
4088
+ )
4089
+ );
4090
+ if (!goal) return;
4091
+ const prop = (key) => {
4092
+ for (let i = from; i < words.length; i++) {
4093
+ const w = words[i];
4094
+ if (w === null) continue;
4095
+ if (w.startsWith(`-D${key}=`)) return w.slice(key.length + 3);
4096
+ if (w === "-D" && words[i + 1]?.startsWith(`${key}=`))
4097
+ return words[i + 1].slice(key.length + 1);
4098
+ }
4099
+ return void 0;
4100
+ };
4101
+ const artifact = prop("artifact");
4102
+ let coord;
4103
+ let ver;
4104
+ if (artifact) {
4105
+ const parts = artifact.split(":");
4106
+ if (parts.length < 2) return;
4107
+ coord = `${parts[0]}:${parts[1]}`;
4108
+ ver = parts[2];
4109
+ } else {
4110
+ const g = prop("groupId");
4111
+ const a = prop("artifactId");
4112
+ if (!g || !a) return;
4113
+ coord = `${g}:${a}`;
4114
+ ver = prop("version");
4115
+ }
4116
+ if (!MAVEN_COORD_RE.test(coord)) return;
4117
+ const exact = ver && !/^(?:LATEST|RELEASE)$/i.test(ver) ? exactVersion(ver) : void 0;
4118
+ push(out, "Maven", "mvn dependency:get", coord, exact, artifact ?? coord);
4119
+ }
4120
+ function composerPackages(words, from, manager, out) {
4121
+ const ops = operands(words, from, COMPOSER_VALUE_FLAGS);
4122
+ const specs = manager === "composer create-project" ? ops.slice(0, 1) : ops;
4123
+ for (const raw of specs) {
4124
+ const sep = raw.search(/[:=]/);
4125
+ const name = sep < 0 ? raw : raw.slice(0, sep);
4126
+ if (!COMPOSER_NAME_RE.test(name)) continue;
4127
+ let ver = sep < 0 ? void 0 : raw.slice(sep + 1);
4128
+ if (manager === "composer create-project" && ver === void 0) ver = ops[2] ?? void 0;
4129
+ push(out, "Packagist", manager, name.toLowerCase(), exactVersion(ver), raw);
4130
+ }
4131
+ }
4132
+ function editorExtensions(words, from, out) {
4133
+ for (const raw of flagValues(words, from, ["--install-extension"])) {
4134
+ if (/\.vsix$/i.test(raw) || NON_REGISTRY_RE.test(raw)) continue;
4135
+ const at = raw.lastIndexOf("@");
4136
+ const name = at <= 0 ? raw : raw.slice(0, at);
4137
+ if (!EXTENSION_ID_RE.test(name)) continue;
4138
+ push(
4139
+ out,
4140
+ "VSCode",
4141
+ "code --install-extension",
4142
+ name,
4143
+ at <= 0 ? void 0 : exactVersion(raw.slice(at + 1)),
4144
+ raw
4145
+ );
4146
+ }
4147
+ }
3667
4148
  function basename(w) {
3668
4149
  return (w ?? "").toLowerCase().split("/").pop() ?? "";
3669
4150
  }
@@ -3895,7 +4376,63 @@ function fromCall(words, out, depth) {
3895
4376
  pushPy(out, "pipenv", operands(words, v + 1, PIPENV_VALUE_FLAGS));
3896
4377
  return;
3897
4378
  }
4379
+ case "gem": {
4380
+ const v = verbIndex(words, start + 1, GEM_VALUE_FLAGS);
4381
+ if (v >= 0 && ["install", "i"].includes(lower(words[v]))) gemInstall(words, v + 1, out);
4382
+ return;
4383
+ }
4384
+ case "bundle": {
4385
+ const v = verbIndex(words, start + 1, BUNDLE_VALUE_FLAGS);
4386
+ if (v < 0 || lower(words[v]) !== "add") return;
4387
+ if (flagValues(words, v + 1, ["--git", "--github", "--path"]).length > 0) return;
4388
+ const version2 = exactVersion(flagValues(words, v + 1, ["-v", "--version"])[0]);
4389
+ for (const raw of operands(words, v + 1, BUNDLE_VALUE_FLAGS))
4390
+ if (GEM_NAME_RE.test(raw)) push(out, "RubyGems", "bundle add", raw, version2, raw);
4391
+ return;
4392
+ }
4393
+ case "cargo": {
4394
+ const from = words[start + 1]?.startsWith("+") ? start + 2 : start + 1;
4395
+ const v = verbIndex(words, from, CARGO_ADD_VALUE_FLAGS);
4396
+ const verb = lower(words[v]);
4397
+ if (verb === "add") cargoCrates(words, v + 1, "cargo add", out);
4398
+ else if (verb === "install") cargoCrates(words, v + 1, "cargo install", out);
4399
+ return;
4400
+ }
4401
+ case "go": {
4402
+ const v = verbIndex(words, start + 1, GO_VALUE_FLAGS);
4403
+ const verb = lower(words[v]);
4404
+ if (verb === "get" || verb === "install" || verb === "run")
4405
+ goModules(words, v + 1, `go ${verb}`, out);
4406
+ return;
4407
+ }
4408
+ case "dotnet": {
4409
+ const v = verbIndex(words, start + 1, DOTNET_ADD_VALUE_FLAGS);
4410
+ const verb = lower(words[v]);
4411
+ if (verb === "add") dotnetAdd(words, v + 1, out);
4412
+ else if (verb === "tool" && ["install", "update"].includes(lower(words[v + 1])))
4413
+ dotnetTool(words, v + 2, out);
4414
+ return;
4415
+ }
4416
+ case "nuget": {
4417
+ if (lower(words[start + 1]) === "install") nugetInstall(words, start + 2, out);
4418
+ return;
4419
+ }
4420
+ case "mvn":
4421
+ case "mvnw":
4422
+ mavenGet(words, start + 1, out);
4423
+ return;
4424
+ case "composer": {
4425
+ let v = verbIndex(words, start + 1, COMPOSER_VALUE_FLAGS);
4426
+ if (v >= 0 && lower(words[v]) === "global") v = verbIndex(words, v + 1, COMPOSER_VALUE_FLAGS);
4427
+ const verb = lower(words[v]);
4428
+ if (verb === "require" || verb === "req" || verb === "r")
4429
+ composerPackages(words, v + 1, "composer require", out);
4430
+ else if (verb === "create-project")
4431
+ composerPackages(words, v + 1, "composer create-project", out);
4432
+ return;
4433
+ }
3898
4434
  default:
4435
+ if (EDITOR_HEADS.includes(family)) editorExtensions(words, start + 1, out);
3899
4436
  return;
3900
4437
  }
3901
4438
  }
@@ -5421,12 +5958,13 @@ function gitRepoPath(url) {
5421
5958
  function isDestructiveToolName(bareTool) {
5422
5959
  return DESTRUCTIVE_VERB_RE.test(bareTool);
5423
5960
  }
5424
- function* validCardRuns(text, base2) {
5961
+ function* validCardRuns(text, base2, exemptFractions = true) {
5425
5962
  const re = new RegExp(base2.source, "g");
5426
5963
  let m;
5427
5964
  while ((m = re.exec(text)) !== null) {
5428
5965
  const digits = m[0].replace(/\D/g, "");
5429
- if (!isDecimalFraction(text, m.index, digits.length === m[0].length) && validateLuhn(digits)) {
5966
+ const fraction = exemptFractions && isDecimalFraction(text, m.index, digits.length === m[0].length);
5967
+ if (!fraction && validateLuhn(digits)) {
5430
5968
  yield { index: m.index, length: m[0].length, digits };
5431
5969
  } else {
5432
5970
  re.lastIndex = m.index + 1;
@@ -5439,12 +5977,12 @@ function hasValidCard(text) {
5439
5977
  }
5440
5978
  return false;
5441
5979
  }
5442
- function replaceValidCards(text, replace) {
5980
+ function replaceValidCards(text, replace, { exemptFractions = true } = {}) {
5443
5981
  let out = text;
5444
5982
  for (const base2 of [PII_CC16_RE, PII_CC15_RE]) {
5445
5983
  let res = "";
5446
5984
  let last = 0;
5447
- for (const run of validCardRuns(out, base2)) {
5985
+ for (const run of validCardRuns(out, base2, exemptFractions)) {
5448
5986
  res += out.slice(last, run.index) + replace(run.digits);
5449
5987
  last = run.index + run.length;
5450
5988
  }
@@ -5458,11 +5996,11 @@ function isDecimalFraction(text, index, unseparated) {
5458
5996
  const a = text[index - 2];
5459
5997
  if (sep === ".") {
5460
5998
  if (a !== void 0 && a >= "0" && a <= "9") return true;
5461
- return a === void 0 || NUMBER_LEAD_RE.test(a);
5999
+ return a === void 0 || (a === "-" || a === "+") && index === 2;
5462
6000
  }
5463
6001
  if (sep === "," && a === "0") {
5464
6002
  const b = text[index - 3];
5465
- return b === void 0 || NUMBER_LEAD_RE.test(b);
6003
+ return b === void 0 || b === ";";
5466
6004
  }
5467
6005
  return false;
5468
6006
  }
@@ -5598,6 +6136,9 @@ function normalizeTrustedEntry(raw) {
5598
6136
  return full === null ? null : trustedEntryOf(full);
5599
6137
  }
5600
6138
  function maskPersonalData(text) {
6139
+ return maskText(text, false);
6140
+ }
6141
+ function maskText(text, exemptFractions) {
5601
6142
  if (!text) return text;
5602
6143
  try {
5603
6144
  let out = text;
@@ -5605,7 +6146,9 @@ function maskPersonalData(text) {
5605
6146
  const v = m.replace(/[ -]/g, "").toUpperCase();
5606
6147
  return validateIban(v) ? `${v.slice(0, 4)} **** **** ${v.slice(-4)}` : m;
5607
6148
  });
5608
- out = replaceValidCards(out, (digits) => `**** **** **** ${digits.slice(-4)}`);
6149
+ out = replaceValidCards(out, (digits) => `**** **** **** ${digits.slice(-4)}`, {
6150
+ exemptFractions
6151
+ });
5609
6152
  out = out.replace(
5610
6153
  ACCOUNT_VALUE_RE,
5611
6154
  (m, v) => IBAN_LENGTH[v.slice(0, 2).toUpperCase()] === v.length ? `"****${v.slice(-4)}"` : m
@@ -5628,7 +6171,7 @@ function maskLeaf(value, key, depth) {
5628
6171
  if (depth > MAX_MASK_DEPTH) return "[REDACTED]";
5629
6172
  if (typeof value === "string") {
5630
6173
  try {
5631
- const json = `{${JSON.stringify(key)}:${JSON.stringify(value)}}`;
6174
+ const json = `{${JSON.stringify(key)}:${JSON.stringify(maskPersonalData(value))}}`;
5632
6175
  return Object.values(JSON.parse(maskPersonalData(json)))[0];
5633
6176
  } catch {
5634
6177
  return "[REDACTED]";
@@ -5636,13 +6179,20 @@ function maskLeaf(value, key, depth) {
5636
6179
  }
5637
6180
  if (typeof value === "number") {
5638
6181
  const text = String(value);
5639
- const masked = maskPersonalData(text);
6182
+ const masked = maskText(text, true);
5640
6183
  return masked === text ? value : masked;
5641
6184
  }
5642
6185
  if (Array.isArray(value)) return value.map((v) => maskLeaf(v, key, depth + 1));
5643
6186
  if (value && typeof value === "object") {
6187
+ const used = /* @__PURE__ */ new Set();
5644
6188
  return Object.fromEntries(
5645
- Object.entries(value).map(([k, v]) => [maskPersonalData(k), maskLeaf(v, k, depth + 1)])
6189
+ Object.entries(value).map(([k, v]) => {
6190
+ const base2 = maskPersonalData(k);
6191
+ let name = base2;
6192
+ for (let n = 2; used.has(name); n++) name = `${base2}#${n}`;
6193
+ used.add(name);
6194
+ return [name, maskLeaf(v, k, depth + 1)];
6195
+ })
5646
6196
  );
5647
6197
  }
5648
6198
  return value;
@@ -6650,7 +7200,7 @@ function* stringValues(obj, depth = 0) {
6650
7200
  }
6651
7201
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
6652
7202
  }
6653
- var aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, CHECK_GROUPS, ALL, BUILTIN_DANGEROUS_WORDS, PRODUCT_CHECKS, SHIELD_RULE_NAME, SHIELD_RULES_FOLDED_INTO_PRODUCT, CHECKS, CHECK_BY_ID, LEGACY_TO_ID, PROJECT_JAIL_BLOCK_READ, OVERRIDE_PREFIX, LABEL_WRAPPERS, LEGACY_READERS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, defaultExpansionOps, VARIABLE_READERS, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, BASH_TOOL_NAMES, MCP_EXECUTORS, BUILTIN_FIELDS, ARGV_FIELDS, BARE_WORD, MAX_ARGV_CHARACTERS, syntax2, MAX_ALTERNATIVES, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, AGENT_INSTRUCTION_FILES, AGENT_INSTRUCTION_DIRS, LEGACY_SYSTEM_PATH_RE, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, MAX_MNEMONIC_HASHES, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, MAX_BLOBS, MIN_DECODED, BASE64_RUN_RE, HEX_RUN_RE, HEX_ESCAPE_RUN_RE, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, BATCH_SEPARATOR, syntax3, NPM_INSTALL_VERBS, NPM_EXEC_VERBS, PNPM_INSTALL_VERBS, YARN_INSTALL_VERBS, BUN_INSTALL_VERBS, DLX_VERBS, NON_REGISTRY_RE, NPM_NAME_RE, EXACT_SEMVER_RE, NPM_VALUE_FLAGS, PNPM_VALUE_FLAGS, YARN_VALUE_FLAGS, BUN_VALUE_FLAGS, PIP_VALUE_FLAGS, UV_VALUE_FLAGS, POETRY_VALUE_FLAGS, PIPENV_VALUE_FLAGS, PIPX_VALUE_FLAGS, PY_SPEC_RE, PY_EXACT_RE, MANAGER_HEADS, SHELLS, MAX_NESTING, SHELL_VALUE_FLAGS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, SECRET_VIEW_LIMITS, ARGUMENT_SCAN_LIMITS, RUN_CHAR, runChar, VERDICT_RANK, SQL_DML_KEYWORDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, DESTRUCTIVE_VERB_RE, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, NUMBER_LEAD_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, MASKED_KINDS, ENTRY_RE, IBAN_TEXT_RE, EMAIL_TEXT_RE, ACCOUNT_KEY_RE, ACCOUNT_VALUE_RE, MAX_MASK_DEPTH, syntax4, EMPTY, CURL_SEND_FLAGS, WGET_SEND_FLAGS, IWR_SEND_FLAGS, WRITE_METHODS, GH_READ, GH_WRITE, GH_HOSTNAME_FLAGS, GH_REPO_VALUE_FLAGS, GH_API_VALUE_FLAGS, GIT_FETCH, PUBLISH, SHELLS2, OWN_HEADS, MAX_DEPTH3, CWD_CHANGERS, TERMINAL_ESCAPE_RE, CONTROL_CHAR_RE, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, MAX_BLAST_PATH, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH4, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, ENGINE_VERSION;
7203
+ var aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, CHECK_GROUPS, ALL, BUILTIN_DANGEROUS_WORDS, PRODUCT_CHECKS, SHIELD_RULE_NAME, SHIELD_RULES_FOLDED_INTO_PRODUCT, CHECKS, CHECK_BY_ID, LEGACY_TO_ID, PROJECT_JAIL_BLOCK_READ, OVERRIDE_PREFIX, LABEL_WRAPPERS, LEGACY_READERS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, defaultExpansionOps, VARIABLE_READERS, FS_READ_TOOLS, GREP_SHAPE, PATTERN_VERBS, PATTERN_VERB_NAMES, GREP_FILE_OPERANDS, READER_VALUE_LETTERS, FILE_OPERAND_FLAGS, SCP_VALUE_FLAGS, TAR_VALUE_LETTERS, ZIP_VALUE_LETTERS, RSYNC_VALUE_LETTERS, RSYNC_SKIP, CP_VALUE_LETTERS, INSTALL_VALUE_LETTERS, COPY_VERBS, TAR_MODE_WORD, COPY_VERB_HEADS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, FIND_EXEC_FLAGS, FS_MUTATION_TOOLS, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, HOME_VARIABLES, assignmentTable, currentStmtOffset, PAYLOAD_BUDGET, payloadBudget, seenPayloads, UNANALYSABLE_NESTING, ASSIGNMENT_HEADS, AND_OP, OR_OP, AND_OR_OPS, MAX_CALL_DEPTH, MAX_CALL_WALKS, callDepth, callWalks, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_FILE_IN_OPS, REDIR_HEREDOC_OPS, FIND_OPTIONS, COPY_RULE_OF, isReaderWord, positionalAfter, NONE, UNKNOWN, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, BASH_TOOL_NAMES, MCP_EXECUTORS, BUILTIN_FIELDS, ARGV_FIELDS, BARE_WORD, MAX_ARGV_CHARACTERS, syntax2, MAX_ALTERNATIVES, MAX_CALL_DEPTH2, MAX_CALL_WALKS2, POSITIONAL, MAX_DEPTH, MAX_PAYLOAD_DEPTH, unique, clone, SENDING_STDIN, DOWNLOAD_SHORT_VALUE, DOWNLOAD_LONG_VALUE, DOWNLOAD_OUTPUT_FLAGS, DOWNLOAD_DIR_FLAGS, base, quoteGlob, asWord, WRITES, READS, DUP_OUT, HERE_STRING, AND, OR, DEFAULT_OPS, SYSTEM_AUTH_FILES, SYSTEM_AUTH_DIRS, PASSWORD_HASH_FILES, STARTUP_NAMES, SYSTEM_DIRS, SYSTEM_DIRS_EXEMPT, UPLOAD_SYSTEM_DIRS, AGENT_INSTRUCTION_FILES, AGENT_INSTRUCTION_DIRS, LEGACY_SYSTEM_PATH_RE, exempt, RANK, BIP39_ENGLISH, BIP39_PUBLIC_VECTORS, indices, lengths, hash, MAX_MNEMONIC_HASHES, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, MAX_BLOBS, MIN_DECODED, BASE64_RUN_RE, HEX_RUN_RE, HEX_ESCAPE_RUN_RE, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, REFERENCE_VALUE_RE, IDENTIFIER_VALUE_RE, PATH_VALUE_RE, DOTTED_REFERENCE_RE, CONCATENATION_RE, EMAIL_VALUE_RE, FIXTURE_NAME_RE, DERIVATIVE_NAME_RE, QUERY_CREDENTIAL_VALUE_RE, OTP_SECRET_RE, OTP_DOC_SECRETS, ASSIGNED_SECRET_VALUE_RE, ASSIGNED_SECRET_NAME_RE, NAME_KEYWORD_RE, ARN_CONTEXT_RE, BASIC_VALUE_RE, BASIC_PAIR_RE, OTP_CAPTURE_RE, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, SENSITIVE_PATH_REGEXES, MAX_DEPTH2, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, BATCH_SEPARATOR, syntax3, NPM_INSTALL_VERBS, NPM_EXEC_VERBS, PNPM_INSTALL_VERBS, YARN_INSTALL_VERBS, BUN_INSTALL_VERBS, DLX_VERBS, NON_REGISTRY_RE, NPM_NAME_RE, EXACT_SEMVER_RE, NPM_VALUE_FLAGS, PNPM_VALUE_FLAGS, YARN_VALUE_FLAGS, BUN_VALUE_FLAGS, PIP_VALUE_FLAGS, UV_VALUE_FLAGS, POETRY_VALUE_FLAGS, PIPENV_VALUE_FLAGS, PIPX_VALUE_FLAGS, PY_SPEC_RE, PY_EXACT_RE, EXACT_VERSION_RE, GEM_NAME_RE, GEM_VALUE_FLAGS, BUNDLE_VALUE_FLAGS, CRATE_NAME_RE, CARGO_ADD_VALUE_FLAGS, CARGO_INSTALL_VALUE_FLAGS, GO_VALUE_FLAGS, GO_PATH_RE, NUGET_NAME_RE, DOTNET_ADD_VALUE_FLAGS, DOTNET_TOOL_VALUE_FLAGS, NUGET_VALUE_FLAGS, MAVEN_COORD_RE, COMPOSER_NAME_RE, COMPOSER_VALUE_FLAGS, EDITOR_HEADS, EXTENSION_ID_RE, MANAGER_HEADS, SHELLS, MAX_NESTING, SHELL_VALUE_FLAGS, TUNING_FIELDS, MAP_GOVERNED, KNOB_GOVERNED, CONFIGURABLE_CHECK_IDS, DEFAULT_EGRESS_ALLOWLIST, PRIVATE_HOST_SUFFIXES, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, DESTINATION_ARGS, SECRET_VIEW_LIMITS, ARGUMENT_SCAN_LIMITS, RUN_CHAR, runChar, VERDICT_RANK, SQL_DML_KEYWORDS, MAX_DESTINATIONS, MAX_SCAN_CHARS, EMAIL_RE, URL_RE, IBAN_RE, RECIPIENT_KEYS, DESTRUCTIVE_VERB_RE, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, MASKED_KINDS, ENTRY_RE, IBAN_TEXT_RE, EMAIL_TEXT_RE, ACCOUNT_KEY_RE, ACCOUNT_VALUE_RE, MAX_MASK_DEPTH, syntax4, EMPTY, CURL_SEND_FLAGS, WGET_SEND_FLAGS, IWR_SEND_FLAGS, WRITE_METHODS, GH_READ, GH_WRITE, GH_HOSTNAME_FLAGS, GH_REPO_VALUE_FLAGS, GH_API_VALUE_FLAGS, GIT_FETCH, PUBLISH, SHELLS2, OWN_HEADS, MAX_DEPTH3, CWD_CHANGERS, TERMINAL_ESCAPE_RE, CONTROL_CHAR_RE, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, MAX_BLAST_PATH, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH4, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, ENGINE_VERSION;
6654
7204
  var init_dist = __esm({
6655
7205
  "packages/policy-engine/dist/index.mjs"() {
6656
7206
  "use strict";
@@ -7451,6 +8001,11 @@ var init_dist = __esm({
7451
8001
  example: "cat /etc/shadow",
7452
8002
  advice: "Keep Review, or choose Block when agents must never read system password hashes."
7453
8003
  },
8004
+ "files.system-dirs": {
8005
+ plain: "The agent writes, creates or deletes a file in a system directory, or sends a system file in a request. /usr/local and a container working directory under /usr/src are ordinary project space and are not included.",
8006
+ example: "wget -O /usr/bin/tool https://example.com/tool",
8007
+ advice: "Keep Review on a workstation. A container image build that installs into /usr can set it to Log."
8008
+ },
7454
8009
  // ── Commands ──────────────────────────────────────────────────────────────
7455
8010
  "commands.inline-exec": {
7456
8011
  plain: "The agent runs a small program written inside the command itself.",
@@ -7647,7 +8202,7 @@ var init_dist = __esm({
7647
8202
  advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
7648
8203
  },
7649
8204
  "loading.malicious-package": {
7650
- plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago.",
8205
+ plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago. Covers npm, PyPI, RubyGems, crates.io, Go, NuGet, Maven, Packagist and VS Code extensions; the publish-age and install-script signals are npm and PyPI only.",
7651
8206
  example: "npm install of a package flagged as malware",
7652
8207
  advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
7653
8208
  }
@@ -7693,6 +8248,15 @@ var init_dist = __esm({
7693
8248
  defaultValue: "review",
7694
8249
  legacy: ["review-password-hash-read"]
7695
8250
  },
8251
+ {
8252
+ id: "files.system-dirs",
8253
+ group: "files",
8254
+ title: "System directories",
8255
+ catches: "Writes, new files and removals under /usr, /etc, /boot, /bin, /sbin, /lib and /var/log, and uploads of system files",
8256
+ values: ALL,
8257
+ defaultValue: "review",
8258
+ legacy: ["review-system-dir-mutation", "review-system-file-upload"]
8259
+ },
7696
8260
  // ── Commands ────────────────────────────────────────────────────────────
7697
8261
  {
7698
8262
  id: "commands.inline-exec",
@@ -8990,6 +9554,10 @@ var init_dist = __esm({
8990
9554
  AND_OP = probeBinOp("a && b");
8991
9555
  OR_OP = probeBinOp("a || b");
8992
9556
  AND_OR_OPS = new Set([AND_OP, OR_OP].filter((o) => o !== null));
9557
+ MAX_CALL_DEPTH = 4;
9558
+ MAX_CALL_WALKS = 256;
9559
+ callDepth = 0;
9560
+ callWalks = 0;
8993
9561
  FS_OP_CACHE_MAX = 5e3;
8994
9562
  fsOpCache = /* @__PURE__ */ new Map();
8995
9563
  stripDotSlash = (p) => p.replace(/^\.\//, "");
@@ -9042,10 +9610,79 @@ var init_dist = __esm({
9042
9610
  MAX_ARGV_CHARACTERS = 262144;
9043
9611
  ({ syntax: syntax2 } = mvdanSh2);
9044
9612
  MAX_ALTERNATIVES = 64;
9613
+ MAX_CALL_DEPTH2 = 4;
9614
+ MAX_CALL_WALKS2 = 256;
9615
+ POSITIONAL = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*", "#"];
9045
9616
  MAX_DEPTH = 64;
9046
9617
  MAX_PAYLOAD_DEPTH = 24;
9047
9618
  unique = (a) => [...new Set(a)].slice(0, MAX_ALTERNATIVES);
9048
- clone = (s) => ({ cwd: [...s.cwd], vars: new Map(s.vars) });
9619
+ clone = (s) => ({
9620
+ cwd: [...s.cwd],
9621
+ vars: new Map(s.vars),
9622
+ ...s.callDepth ? { callDepth: s.callDepth } : {}
9623
+ });
9624
+ SENDING_STDIN = /* @__PURE__ */ new Set(["nc", "ncat", "netcat", "socat", "telnet", "curl", "wget"]);
9625
+ DOWNLOAD_SHORT_VALUE = {
9626
+ curl: "odDeEFHKmPrtTuUwxXyYzAbcCQ",
9627
+ wget: "OoaPiBetTwQlARDXIUY",
9628
+ aria2c: "odisx"
9629
+ };
9630
+ DOWNLOAD_LONG_VALUE = {
9631
+ curl: [
9632
+ "--output",
9633
+ "--output-dir",
9634
+ "--dump-header",
9635
+ "--cookie-jar",
9636
+ "--data",
9637
+ "--data-binary",
9638
+ "--data-raw",
9639
+ "--data-ascii",
9640
+ "--data-urlencode",
9641
+ "--form",
9642
+ "--header",
9643
+ "--request",
9644
+ "--user",
9645
+ "--user-agent",
9646
+ "--referer",
9647
+ "--cookie",
9648
+ "--config",
9649
+ "--json",
9650
+ "--upload-file",
9651
+ "--max-time",
9652
+ "--proxy",
9653
+ "--write-out",
9654
+ "--url"
9655
+ ],
9656
+ wget: [
9657
+ "--output-document",
9658
+ "--output-file",
9659
+ "--append-output",
9660
+ "--directory-prefix",
9661
+ "--input-file",
9662
+ "--post-data",
9663
+ "--post-file",
9664
+ "--body-data",
9665
+ "--body-file",
9666
+ "--method",
9667
+ "--header",
9668
+ "--user-agent",
9669
+ "--user",
9670
+ "--password",
9671
+ "--tries",
9672
+ "--timeout"
9673
+ ],
9674
+ aria2c: ["--out", "--dir", "--input-file"]
9675
+ };
9676
+ DOWNLOAD_OUTPUT_FLAGS = {
9677
+ curl: ["-o", "--output", "-D", "--dump-header", "-c", "--cookie-jar"],
9678
+ wget: ["-O", "--output-document", "-o", "--output-file", "-a", "--append-output"],
9679
+ aria2c: ["-o", "--out"]
9680
+ };
9681
+ DOWNLOAD_DIR_FLAGS = {
9682
+ curl: ["--output-dir"],
9683
+ wget: ["-P", "--directory-prefix"],
9684
+ aria2c: ["-d", "--dir"]
9685
+ };
9049
9686
  base = (s) => (s ?? "").replace(/\/+$/, "").split("/").pop() ?? "";
9050
9687
  quoteGlob = (s) => s.replace(/[\\*?\[\]{}()!+@]/g, "\\$&");
9051
9688
  asWord = (p) => ({ value: p.value, ...p.glob ? { pattern: p.pattern } : {} });
@@ -9070,6 +9707,18 @@ var init_dist = __esm({
9070
9707
  SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
9071
9708
  PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
9072
9709
  STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
9710
+ SYSTEM_DIRS = [
9711
+ "/usr",
9712
+ "/etc",
9713
+ "/boot",
9714
+ "/bin",
9715
+ "/sbin",
9716
+ "/lib",
9717
+ "/lib64",
9718
+ "/var/log"
9719
+ ];
9720
+ SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
9721
+ UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
9073
9722
  AGENT_INSTRUCTION_FILES = [
9074
9723
  ".gitlab-ci.yml",
9075
9724
  "CLAUDE.md",
@@ -9087,6 +9736,7 @@ var init_dist = __esm({
9087
9736
  ".husky"
9088
9737
  ];
9089
9738
  LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
9739
+ exempt = (p) => SYSTEM_DIRS_EXEMPT.some((d) => within(p, d));
9090
9740
  RANK = { allow: 0, review: 1, block: 2 };
9091
9741
  BIP39_ENGLISH = `abandon
9092
9742
  ability
@@ -11413,15 +12063,20 @@ zoo`.split("\n");
11413
12063
  IDENTIFIER_VALUE_RE = /^[A-Za-z_.-]+$/;
11414
12064
  PATH_VALUE_RE = /^(?:\/|\.{1,2}\/|~\/)/;
11415
12065
  DOTTED_REFERENCE_RE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/;
12066
+ CONCATENATION_RE = /^\+[A-Za-z_$][\w$]*\./;
11416
12067
  EMAIL_VALUE_RE = /^[^@\s]+@[^@\s]+\.[A-Za-z]{2,}$/;
11417
12068
  FIXTURE_NAME_RE = /(?:fake|dummy|example|sample|mock|stub|placeholder|test)/i;
11418
- DERIVATIVE_NAME_RE = /(?:hash|digest|id|type|name|url|uri|path|file|len|length|count|size|ttl|expires?|expiry|age|date|time|format|field|header|param)$/i;
12069
+ DERIVATIVE_NAME_RE = /(?:hash|digest|fingerprint|thumbprint|checksum|id|type|name|url|uri|path|file|len|length|count|size|ttl|expires?|expiry|age|date|time|format|field|header|param)$/i;
11419
12070
  QUERY_CREDENTIAL_VALUE_RE = /=([A-Za-z0-9%+/_.~-]{8,512}={0,2})$/;
11420
12071
  OTP_SECRET_RE = /^[A-Z2-7]{16,}=*$/i;
11421
- ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
12072
+ OTP_DOC_SECRETS = /* @__PURE__ */ new Set(["JBSWY3DPEHPK3PXP", "GEZDGNBVGY3TQOJQ"]);
12073
+ ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
11422
12074
  ASSIGNED_SECRET_NAME_RE = /^([A-Za-z0-9_]+)/;
11423
- NAME_KEYWORD_RE = /^(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret)/i;
12075
+ NAME_KEYWORD_RE = /^(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret|consumer[_-]?key|app[_-]?key|signing[_-]?key|encryption[_-]?key|master[_-]?key|jsapi[_-]?ticket|pass)/i;
11424
12076
  ARN_CONTEXT_RE = /\barn:[A-Za-z0-9-]*:[^\s"'`]*$/i;
12077
+ BASIC_VALUE_RE = /([A-Za-z0-9+/]{8,512}={0,2})$/;
12078
+ BASIC_PAIR_RE = /^([^:\s]{1,128}):(\S{8,256})$/;
12079
+ OTP_CAPTURE_RE = /["']([A-Z2-7]{16,128}=*)["']$/i;
11425
12080
  DLP_PATTERNS = [
11426
12081
  // ── AWS ───────────────────────────────────────────────────────────────────
11427
12082
  {
@@ -11759,6 +12414,22 @@ zoo`.split("\n");
11759
12414
  contextBoost: true,
11760
12415
  minEntropy: 3
11761
12416
  },
12417
+ // ── HTTP Basic credential ─────────────────────────────────────────────────
12418
+ // `Authorization: Basic <base64 of user:password>`, or the bare scheme in a
12419
+ // config list. The validator decodes the value and accepts only a
12420
+ // `user:password` whose password looks generated (letters and digits, 8+
12421
+ // characters, the query-credential rule), so prose ("Basic authentication")
12422
+ // and documentation pairs (`admin:changeme`, `user:pass1234`) stay allowed.
12423
+ // review: the same weight as a bare Bearer token.
12424
+ {
12425
+ name: "HTTP Basic Credential",
12426
+ regex: /\bBasic[ \t]{1,8}([A-Za-z0-9+/]{8,512}={0,2})/i,
12427
+ severity: "review",
12428
+ keywords: ["basic"],
12429
+ validate: validateBasicCredential,
12430
+ redactGroup: 1,
12431
+ argumentsOnly: true
12432
+ },
11762
12433
  // ── Resend ────────────────────────────────────────────────────────────────
11763
12434
  {
11764
12435
  name: "Resend API Key",
@@ -11900,6 +12571,19 @@ zoo`.split("\n");
11900
12571
  validate: validateQueryCredential,
11901
12572
  argumentsOnly: true
11902
12573
  },
12574
+ // ── OTP secret in a constructor ───────────────────────────────────────────
12575
+ // `ROTP::TOTP.new('<base32>')`, `pyotp.TOTP("<base32>")`, `new HOTP('...')`:
12576
+ // the shared secret of a one-time-password generator written as a literal.
12577
+ // The otpauth URI form is `Credential in URL Query`. review.
12578
+ {
12579
+ name: "OTP Secret",
12580
+ regex: /\b(?:TOTP|HOTP|OTP)(?:\.new|\.create)?[ \t]{0,8}\(?[ \t]{0,8}["']([A-Z2-7]{16,128}=*)["']/i,
12581
+ severity: "review",
12582
+ keywords: ["otp"],
12583
+ validate: validateOtpSecret,
12584
+ redactGroup: 1,
12585
+ argumentsOnly: true
12586
+ },
11903
12587
  // ── Secret assigned to a telling name ─────────────────────────────────────
11904
12588
  // `password: ...`, `JWT_SECRET=...`, `api_key = "..."`. The name alone is
11905
12589
  // not a finding: most such lines hold a reference (`process.env.X`), a
@@ -11907,12 +12591,14 @@ zoo`.split("\n");
11907
12591
  // generated: 12+ characters, not an identifier, Shannon entropy of 3.5
11908
12592
  // bits per character or more. review, never block (node9Firewall#348,
11909
12593
  // item 4). Tuned on CredData's public split; its look-alike lines are the
11910
- // negative control.
12594
+ // negative control. A quote may be escaped (`\"access_token\":\"...\"`):
12595
+ // JSON written inside a string literal. The two optional quotes after the
12596
+ // name are written out, not as a counted group, to keep star height 1.
11911
12597
  {
11912
12598
  name: "Assigned Secret",
11913
- regex: /(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret)[A-Za-z0-9_]{0,40}["'\]]{0,2}[ \t]{0,8}(?::=|=>|[:=])[ \t]{0,8}["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})/i,
12599
+ regex: /(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret|consumer[_-]?key|app[_-]?key|signing[_-]?key|encryption[_-]?key|master[_-]?key|jsapi[_-]?ticket|\bpass(?![A-Za-z0-9])|_pass(?![A-Za-z0-9]))[A-Za-z0-9_]{0,40}\\?["'\]]?\\?["'\]]?[ \t]{0,8}(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})/i,
11914
12600
  severity: "review",
11915
- keywords: ["password", "passphrase", "passwd", "pwd", "secret", "token", "key"],
12601
+ keywords: ["pass", "pwd", "secret", "token", "key", "ticket"],
11916
12602
  validate: validateAssignedSecret,
11917
12603
  argumentsOnly: true
11918
12604
  }
@@ -12180,7 +12866,169 @@ zoo`.split("\n");
12180
12866
  ]);
12181
12867
  PY_SPEC_RE = /^([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)(?:\[[^\]]*\])?\s*(.*)$/;
12182
12868
  PY_EXACT_RE = /^===?\s*v?(\d+(?:\.\d+)*(?:[a-zA-Z0-9.+!-]*))$/;
12869
+ EXACT_VERSION_RE = /^=?v?(\d+(?:\.\d+)*(?:[-+][0-9A-Za-z][0-9A-Za-z.+-]*)?)$/;
12870
+ GEM_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
12871
+ GEM_VALUE_FLAGS = /* @__PURE__ */ new Set([
12872
+ "-v",
12873
+ "--version",
12874
+ "-i",
12875
+ "--install-dir",
12876
+ "-n",
12877
+ "--bindir",
12878
+ "--document",
12879
+ "--build-root",
12880
+ "-P",
12881
+ "--trust-policy",
12882
+ "-s",
12883
+ "--source",
12884
+ "-p",
12885
+ "--http-proxy",
12886
+ "--platform",
12887
+ "-g",
12888
+ "--file",
12889
+ "--without"
12890
+ ]);
12891
+ BUNDLE_VALUE_FLAGS = /* @__PURE__ */ new Set([
12892
+ "-v",
12893
+ "--version",
12894
+ "-g",
12895
+ "--group",
12896
+ "-s",
12897
+ "--source",
12898
+ "-r",
12899
+ "--require",
12900
+ "--path",
12901
+ "--git",
12902
+ "--github",
12903
+ "--branch",
12904
+ "--ref",
12905
+ "--glob"
12906
+ ]);
12907
+ CRATE_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_-]*$/u;
12908
+ CARGO_ADD_VALUE_FLAGS = /* @__PURE__ */ new Set([
12909
+ "-F",
12910
+ "--features",
12911
+ "--rename",
12912
+ "--registry",
12913
+ "--manifest-path",
12914
+ "-p",
12915
+ "--package",
12916
+ "--git",
12917
+ "--branch",
12918
+ "--tag",
12919
+ "--rev",
12920
+ "--path",
12921
+ "--base",
12922
+ "--target",
12923
+ "--color",
12924
+ "--config",
12925
+ "-Z",
12926
+ "--lockfile-path"
12927
+ ]);
12928
+ CARGO_INSTALL_VALUE_FLAGS = /* @__PURE__ */ new Set([
12929
+ "--version",
12930
+ "--vers",
12931
+ "--git",
12932
+ "--branch",
12933
+ "--tag",
12934
+ "--rev",
12935
+ "--path",
12936
+ "--root",
12937
+ "--index",
12938
+ "--registry",
12939
+ "-F",
12940
+ "--features",
12941
+ "--profile",
12942
+ "--target",
12943
+ "--target-dir",
12944
+ "-j",
12945
+ "--jobs",
12946
+ "--bin",
12947
+ "--example",
12948
+ "--color",
12949
+ "--config",
12950
+ "-Z"
12951
+ ]);
12952
+ GO_VALUE_FLAGS = /* @__PURE__ */ new Set([
12953
+ "-C",
12954
+ "-tags",
12955
+ "-ldflags",
12956
+ "-gcflags",
12957
+ "-asmflags",
12958
+ "-mod",
12959
+ "-modfile",
12960
+ "-o",
12961
+ "-p",
12962
+ "-pkgdir",
12963
+ "-toolexec",
12964
+ "-exec",
12965
+ "-overlay",
12966
+ "-pgo",
12967
+ "-buildmode",
12968
+ "-compiler",
12969
+ "-gccgoflags",
12970
+ "-installsuffix",
12971
+ "-covermode",
12972
+ "-coverpkg"
12973
+ ]);
12974
+ GO_PATH_RE = /^[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+(?:\/[\p{L}\p{N}_.~+-]+)*$/u;
12975
+ NUGET_NAME_RE = /^[\p{L}\p{N}_][\p{L}\p{N}_.-]*$/u;
12976
+ DOTNET_ADD_VALUE_FLAGS = /* @__PURE__ */ new Set([
12977
+ "-v",
12978
+ "--version",
12979
+ "-s",
12980
+ "--source",
12981
+ "-f",
12982
+ "--framework",
12983
+ "--package-directory"
12984
+ ]);
12985
+ DOTNET_TOOL_VALUE_FLAGS = /* @__PURE__ */ new Set([
12986
+ "--version",
12987
+ "--add-source",
12988
+ "--configfile",
12989
+ "--framework",
12990
+ "--tool-path",
12991
+ "-a",
12992
+ "--arch",
12993
+ "--tool-manifest",
12994
+ "-v",
12995
+ "--verbosity"
12996
+ ]);
12997
+ NUGET_VALUE_FLAGS = /* @__PURE__ */ new Set([
12998
+ "-version",
12999
+ "-outputdirectory",
13000
+ "-o",
13001
+ "-source",
13002
+ "-configfile",
13003
+ "-fallbacksource",
13004
+ "-framework",
13005
+ "-packagesavemode",
13006
+ "-dependencyversion",
13007
+ "-solutiondirectory",
13008
+ "-verbosity"
13009
+ ]);
13010
+ MAVEN_COORD_RE = /^[\p{L}\p{N}_.-]+:[\p{L}\p{N}_.-]+$/u;
13011
+ COMPOSER_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_.-]*\/[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
13012
+ COMPOSER_VALUE_FLAGS = /* @__PURE__ */ new Set([
13013
+ "-d",
13014
+ "--working-dir",
13015
+ "--stability",
13016
+ "--repository",
13017
+ "--add-repository"
13018
+ ]);
13019
+ EDITOR_HEADS = ["code", "code-insiders", "codium", "vscodium", "cursor"];
13020
+ EXTENSION_ID_RE = /^[\p{L}\p{N}][\p{L}\p{N}_-]*\.[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
12183
13021
  MANAGER_HEADS = /* @__PURE__ */ new Set([
13022
+ "gem",
13023
+ "bundle",
13024
+ "cargo",
13025
+ "go",
13026
+ "dotnet",
13027
+ "nuget",
13028
+ "mvn",
13029
+ "mvnw",
13030
+ "composer",
13031
+ ...EDITOR_HEADS,
12184
13032
  "npm",
12185
13033
  "npx",
12186
13034
  "pnpm",
@@ -12259,6 +13107,7 @@ zoo`.split("\n");
12259
13107
  MAP_GOVERNED = /* @__PURE__ */ new Set([
12260
13108
  "files.shell-startup",
12261
13109
  "files.password-hashes",
13110
+ "files.system-dirs",
12262
13111
  "commands.inline-exec",
12263
13112
  "commands.eval-dynamic",
12264
13113
  "commands.curl-pipe-shell",
@@ -12527,7 +13376,6 @@ zoo`.split("\n");
12527
13376
  PII_CC16_RE = /\b(?:4\d{3}|5[1-5]\d{2}|6\d{3})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/;
12528
13377
  PII_CC15_RE = /\b3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}\b/;
12529
13378
  PII_IBAN_RE = /\b[A-Z]{2}\d{2}(?:[A-Z0-9]|[ -][A-Z0-9]){11,30}\b/;
12530
- NUMBER_LEAD_RE = /[\s:=;([{"'+-]/;
12531
13379
  REALTIME_PII_PATTERNS = ["SSN", "Credit Card", "IBAN"];
12532
13380
  MAX_PII_SCAN_BYTES = 1e5;
12533
13381
  MASKED_KINDS = /* @__PURE__ */ new Set(["email", "iban", "account"]);
@@ -12749,7 +13597,7 @@ zoo`.split("\n");
12749
13597
  { view: "separators-stripped", decoder: "separators", stripped: true }
12750
13598
  ];
12751
13599
  LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
12752
- CANONICAL_EXTRACTOR_VERSION = "canonical-v27";
13600
+ CANONICAL_EXTRACTOR_VERSION = "canonical-v28";
12753
13601
  DEDUPE_PREVIEW_LEN = 120;
12754
13602
  ENGINE_VERSION = "1.4.0";
12755
13603
  }
@@ -16158,14 +17006,23 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
16158
17006
  clearTimeout(timer);
16159
17007
  }
16160
17008
  }
16161
- async function waitForDaemonDecision(id, signal) {
17009
+ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
16162
17010
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
16163
17011
  const waitCtrl = new AbortController();
16164
- const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
17012
+ let capped = false;
17013
+ const waitTimer = maxWaitMs > 0 ? setTimeout(() => {
17014
+ capped = true;
17015
+ waitCtrl.abort();
17016
+ }, maxWaitMs) : void 0;
16165
17017
  const onAbort = () => waitCtrl.abort();
16166
17018
  if (signal) signal.addEventListener("abort", onAbort);
16167
17019
  try {
16168
- const waitRes = await fetch(`${base2}/wait/${id}`, { signal: waitCtrl.signal });
17020
+ const waitRes = await fetch(`${base2}/wait/${id}`, { signal: waitCtrl.signal }).catch(
17021
+ (err2) => {
17022
+ if (capped && !signal?.aborted) throw new Error("node9 daemon did not answer in time");
17023
+ throw err2;
17024
+ }
17025
+ );
16169
17026
  if (!waitRes.ok) return { decision: "deny" };
16170
17027
  const { decision, source, reason } = await waitRes.json();
16171
17028
  if (decision === "allow") return { decision: "allow", source };
@@ -17791,13 +18648,13 @@ function maskRiskMetadataForCloud(rm) {
17791
18648
  const r = rm;
17792
18649
  const dests = Array.isArray(r.exposureDestinations) ? r.exposureDestinations.filter((d) => typeof d === "string") : [];
17793
18650
  const swaps = dests.filter(isMaskedKind).map((d) => [destinationValue(d), destinationValue(maskDestination(d))]).filter(([full]) => full.length > 0).sort((a, b) => b[0].length - a[0].length);
17794
- const maskText = (text) => {
18651
+ const maskText2 = (text) => {
17795
18652
  let out2 = text;
17796
18653
  for (const [full, masked] of swaps) out2 = out2.split(full).join(masked);
17797
18654
  return maskPersonalData(out2);
17798
18655
  };
17799
18656
  const out = {};
17800
- for (const [k, v] of Object.entries(r)) out[k] = typeof v === "string" ? maskText(v) : v;
18657
+ for (const [k, v] of Object.entries(r)) out[k] = typeof v === "string" ? maskText2(v) : v;
17801
18658
  if (dests.length > 0) {
17802
18659
  out.exposureDestinations = dests.map(maskDestination);
17803
18660
  out.exposureDestinationHashes = dests.map((d) => isMaskedKind(d) ? hashDestination(d) : "");
@@ -18100,7 +18957,20 @@ function ecoDir(eco) {
18100
18957
  return path24.join(osvRoot(), eco);
18101
18958
  }
18102
18959
  function indexKey(eco, name) {
18103
- return eco === "PyPI" ? normalizePyPiName(name) : name.toLowerCase();
18960
+ switch (eco) {
18961
+ case "PyPI":
18962
+ return normalizePyPiName(name);
18963
+ case "crates.io":
18964
+ return name.toLowerCase().replace(/_/g, "-");
18965
+ case "Go":
18966
+ case "Maven":
18967
+ return name;
18968
+ default:
18969
+ return name.toLowerCase();
18970
+ }
18971
+ }
18972
+ function recordEcosystemMatches(eco, recordEco) {
18973
+ return typeof recordEco === "string" && (recordEco === eco || recordEco.startsWith(`${eco}:`));
18104
18974
  }
18105
18975
  function shardOf(key) {
18106
18976
  return createHash5("sha1").update(key).digest("hex").slice(0, 2);
@@ -18146,7 +19016,8 @@ function entriesFromRecord(eco, record2) {
18146
19016
  const modifiedMs = typeof r.modified === "string" ? Date.parse(r.modified) || 0 : 0;
18147
19017
  const rows = /* @__PURE__ */ new Map();
18148
19018
  for (const a of r.affected ?? []) {
18149
- if (a?.package?.ecosystem !== eco || typeof a.package.name !== "string") continue;
19019
+ if (!recordEcosystemMatches(eco, a?.package?.ecosystem) || typeof a?.package?.name !== "string")
19020
+ continue;
18150
19021
  const key = indexKey(eco, a.package.name);
18151
19022
  const entry = rows.get(key) ?? { id: r.id };
18152
19023
  if (Array.isArray(a.versions)) {
@@ -18386,15 +19257,18 @@ async function pypiInfo(name, version2) {
18386
19257
  };
18387
19258
  }
18388
19259
  async function registryInfo(eco, name, version2, freshWindowMs, raw) {
18389
- return eco === "npm" ? npmInfo(name, version2, freshWindowMs, raw) : pypiInfo(name, version2);
19260
+ if (eco === "npm") return npmInfo(name, version2, freshWindowMs, raw);
19261
+ if (eco === "PyPI") return pypiInfo(name, version2);
19262
+ return null;
18390
19263
  }
18391
- var TIMEOUT_MS2, MAX_BYTES;
19264
+ var TIMEOUT_MS2, MAX_BYTES, HAS_REGISTRY_CLIENT;
18392
19265
  var init_registry2 = __esm({
18393
19266
  "src/supply-chain/registry.ts"() {
18394
19267
  "use strict";
18395
19268
  init_net();
18396
19269
  TIMEOUT_MS2 = 1500;
18397
19270
  MAX_BYTES = 2 * 1024 * 1024;
19271
+ HAS_REGISTRY_CLIENT = /* @__PURE__ */ new Set(["npm", "PyPI"]);
18398
19272
  }
18399
19273
  });
18400
19274
 
@@ -18441,25 +19315,39 @@ function commandOf(toolName, args) {
18441
19315
  return typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : null;
18442
19316
  }
18443
19317
  function label(p, version2) {
18444
- return `${p.ecosystem === "npm" ? "npm" : "PyPI"} package "${p.name}${version2 ? `@${version2}` : ""}"`;
19318
+ return `${KIND[p.ecosystem]} "${p.name}${version2 ? `@${version2}` : ""}"`;
19319
+ }
19320
+ function osvNames(p) {
19321
+ if (p.ecosystem !== "Go") return [p.name];
19322
+ const parts = p.name.split("/");
19323
+ const out = [];
19324
+ for (let n = parts.length; n >= 2 && out.length < 6; n--) out.push(parts.slice(0, n).join("/"));
19325
+ return out;
18445
19326
  }
18446
19327
  async function maliciousFor(p, version2, cfg, misses, networkCapped) {
18447
- const local = lookupIndex(p.ecosystem, p.name);
18448
- const entries = local.status === "hit" ? local.entries : null;
18449
- if (!entries && !(local.status === "clean" && local.fresh)) {
19328
+ const names = osvNames(p);
19329
+ const local = names.map((n) => lookupIndex(p.ecosystem, n));
19330
+ const hits2 = local.flatMap((l) => l.status === "hit" ? l.entries : []);
19331
+ const entries = hits2.length > 0 ? hits2 : null;
19332
+ const freshClean = local.every((l) => l.status === "clean" && l.fresh);
19333
+ const status = local.some((l) => l.status === "unavailable") ? "unavailable" : local[0].status;
19334
+ if (!entries && !freshClean) {
18450
19335
  if (!cfg.onlineFallback || networkCapped) {
18451
19336
  misses.push(
18452
- `${label(p, version2)}: local index ${local.status}, no online lookup (${networkCapped ? "over the per-command network cap" : "online fallback off"})`
19337
+ `${label(p, version2)}: local index ${status}, no online lookup (${networkCapped ? "over the per-command network cap" : "online fallback off"})`
18453
19338
  );
18454
19339
  return null;
18455
19340
  }
18456
- let ids = null;
19341
+ let perName = null;
18457
19342
  try {
18458
- ids = await queryOsvMalicious([{ ecosystem: p.ecosystem, name: p.name, version: version2 }]);
19343
+ perName = await queryOsvMalicious(
19344
+ names.map((name) => ({ ecosystem: p.ecosystem, name, version: version2 }))
19345
+ );
18459
19346
  } catch (err2) {
18460
19347
  misses.push(`${label(p, version2)}: OSV online ${err2.message}`);
18461
19348
  return null;
18462
19349
  }
19350
+ const ids = perName ? [[...new Set(perName.flat())]] : null;
18463
19351
  if (!ids) {
18464
19352
  misses.push(`${label(p, version2)}: OSV online unavailable`);
18465
19353
  return null;
@@ -18529,7 +19417,7 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
18529
19417
  }
18530
19418
  const findings = [];
18531
19419
  let info = null;
18532
- const wantsRegistry = cfg.newPackage !== "off" || cfg.installScript !== "off";
19420
+ const wantsRegistry = (cfg.newPackage !== "off" || cfg.installScript !== "off") && HAS_REGISTRY_CLIENT.has(p.ecosystem);
18533
19421
  if (wantsRegistry && !networkCapped) {
18534
19422
  try {
18535
19423
  const windowMs = cfg.newPackage === "off" ? 0 : cfg.maxAgeHours * 36e5;
@@ -18609,7 +19497,7 @@ async function runPackageCheck(toolName, args, cfg, cwd) {
18609
19497
  return { ...ALLOW, misses: [`package check error: ${err2?.message ?? err2}`] };
18610
19498
  }
18611
19499
  }
18612
- var MAX_NETWORK_PACKAGES, ALLOW;
19500
+ var MAX_NETWORK_PACKAGES, ALLOW, KIND;
18613
19501
  var init_check = __esm({
18614
19502
  "src/supply-chain/check.ts"() {
18615
19503
  "use strict";
@@ -18620,6 +19508,164 @@ var init_check = __esm({
18620
19508
  init_local_resolve();
18621
19509
  MAX_NETWORK_PACKAGES = 10;
18622
19510
  ALLOW = { verdict: "allow", findings: [], misses: [], checked: 0 };
19511
+ KIND = {
19512
+ npm: "npm package",
19513
+ PyPI: "PyPI package",
19514
+ RubyGems: "RubyGems package",
19515
+ "crates.io": "crates.io package",
19516
+ Go: "Go module",
19517
+ NuGet: "NuGet package",
19518
+ Maven: "Maven artifact",
19519
+ Packagist: "Packagist package",
19520
+ VSCode: "VS Code extension"
19521
+ };
19522
+ }
19523
+ });
19524
+
19525
+ // src/policy/negotiation.ts
19526
+ function buildNegotiationMessage(blockedByLabel, isHumanDecision2, humanReason, recoveryCommand, context) {
19527
+ if (isHumanDecision2) {
19528
+ return `NODE9: The human user rejected this action.
19529
+ REASON: ${humanReason || "No specific reason provided."}
19530
+ INSTRUCTIONS:
19531
+ - Do NOT retry this exact command.
19532
+ - Acknowledge the block to the user and ask if there is an alternative approach.
19533
+ - If you believe this action is critical, explain your reasoning and ask them to run "node9 pause 15m" to proceed.`;
19534
+ }
19535
+ if (context?.kind === "protected-path") {
19536
+ const subject = context.path ? `${context.path} is a protected file` : "This file is protected";
19537
+ return `NODE9: Blocked. ${subject}, and node9 stopped this call to it. The call did not run, so nothing was exposed.
19538
+ INSTRUCTIONS:
19539
+ - Do NOT retry, and do NOT reach the file another way (cat, grep, a copy, a script, a redirect).
19540
+ - If this step needs that file, ask the user to provide what you need or to do the step themselves.
19541
+ - Tell the user node9 protected this file, then ask how to proceed.`;
19542
+ }
19543
+ const label3 = blockedByLabel.toLowerCase();
19544
+ if (label3.includes("dlp") || label3.includes("secret detected") || label3.includes("credential review")) {
19545
+ return `NODE9 SECURITY ALERT: A sensitive credential (API key, token, or private key) was found in your tool call arguments.
19546
+ CRITICAL INSTRUCTION: Do NOT retry this action.
19547
+ REQUIRED ACTIONS:
19548
+ 1. Remove the hardcoded credential from your command or code.
19549
+ 2. Use an environment variable or a dedicated secrets manager instead.
19550
+ 3. Treat the leaked credential as compromised and rotate it immediately.
19551
+ Do NOT attempt to bypass this check or pass the credential through another tool.
19552
+ - Tell the user node9 blocked this and why, then ask how to proceed.`;
19553
+ }
19554
+ if (label3.includes("package check")) {
19555
+ return `NODE9: Blocked \u2014 ${humanReason || "a package in this command is known to be malicious."}
19556
+ INSTRUCTIONS:
19557
+ - Do NOT install this package, and do NOT fetch it another way (a different package manager, a tarball URL, a git URL, a mirror).
19558
+ - If a dependency is needed, suggest a well-known alternative and let the user choose.
19559
+ - Tell the user node9 blocked this install and name the advisory, then ask how to proceed.`;
19560
+ }
19561
+ if (label3.includes("sql safety") && label3.includes("delete without where")) {
19562
+ return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
19563
+ INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
19564
+ Do NOT retry without a WHERE clause.
19565
+ - Tell the user node9 blocked this and why, then ask how to proceed.`;
19566
+ }
19567
+ if (label3.includes("sql safety") && label3.includes("update without where")) {
19568
+ return `NODE9: Blocked \u2014 UPDATE without WHERE clause would update every row.
19569
+ INSTRUCTION: Add a WHERE clause to scope the update (e.g. WHERE id = <value>).
19570
+ Do NOT retry without a WHERE clause.
19571
+ - Tell the user node9 blocked this and why, then ask how to proceed.`;
19572
+ }
19573
+ if (label3.includes("dangerous word")) {
19574
+ const match = blockedByLabel.match(/dangerous word: "([^"]+)"/i);
19575
+ const word = match?.[1] ?? "a dangerous keyword";
19576
+ return `NODE9: Blocked \u2014 command contains forbidden keyword "${word}".
19577
+ INSTRUCTION: Do NOT use "${word}". Use a non-destructive alternative.
19578
+ Do NOT attempt to bypass this with shell tricks or aliases \u2014 it will be blocked again.
19579
+ - Tell the user node9 blocked this and why, then ask how to proceed.`;
19580
+ }
19581
+ if (label3.includes("path blocked") || label3.includes("sandbox")) {
19582
+ return `NODE9: Blocked \u2014 operation targets a path outside the allowed sandbox.
19583
+ INSTRUCTION: Move your output to an allowed directory such as /tmp/ or the project directory.
19584
+ Do NOT retry on the same path.
19585
+ - Tell the user node9 blocked this and why, then ask how to proceed.`;
19586
+ }
19587
+ if (label3.includes("inline execution")) {
19588
+ return `NODE9: Blocked \u2014 inline code execution (e.g. bash -c "...") is not allowed.
19589
+ INSTRUCTION: Use individual tool calls instead of embedding code in a shell string.
19590
+ - Tell the user node9 blocked this and why, then ask how to proceed.`;
19591
+ }
19592
+ if (label3.includes("strict mode")) {
19593
+ return `NODE9: Blocked \u2014 strict mode is active. All tool calls require explicit human approval.
19594
+ INSTRUCTION: Inform the user this action is pending approval. Wait for them to approve via the dashboard or run "node9 pause".`;
19595
+ }
19596
+ if (label3.includes("rule") && label3.includes("default block")) {
19597
+ const match = blockedByLabel.match(/rule "([^"]+)"/i);
19598
+ const rule = match?.[1] ?? "a policy rule";
19599
+ return `NODE9: Blocked \u2014 action "${rule}" is forbidden by security policy.
19600
+ INSTRUCTION: Do NOT use "${rule}". Find a read-only or non-destructive alternative.
19601
+ Do NOT attempt to bypass this rule.
19602
+ - Tell the user node9 blocked this and why, then ask how to proceed.`;
19603
+ }
19604
+ const recovery = recoveryCommand ? `
19605
+ REQUIRED ACTION: Run \`${recoveryCommand}\` first, then retry your original command.` : "\n- Pivot to a non-destructive or read-only alternative.";
19606
+ return `NODE9: Action blocked by security policy [${agentFacingLabel(blockedByLabel)}].
19607
+ INSTRUCTIONS:
19608
+ - Do NOT retry this exact command or attempt to bypass the rule.${recovery}
19609
+ - Inform the user which security rule was triggered and ask how to proceed.`;
19610
+ }
19611
+ function buildReviewMessage(blockedByLabel, ruleDescription, reason) {
19612
+ const why = ruleDescription || (reason && reason !== blockedByLabel ? reason : void 0) || blockedByLabel || "this action needs your review";
19613
+ const trimmed = why.trimEnd();
19614
+ const sentence2 = /[.!?\u2026]$/.test(trimmed) ? trimmed : `${trimmed}.`;
19615
+ return `Node9 flagged this for your review: ${sentence2} Approve to proceed, or deny to cancel.`;
19616
+ }
19617
+ function isHumanDecision(label3, blockedBy) {
19618
+ if (blockedBy !== void 0 && blockedBy !== "local-decision") return false;
19619
+ const l = label3.toLowerCase();
19620
+ return l.includes("user") || l.includes("daemon") || l.includes("decision");
19621
+ }
19622
+ function agentFacingLabel(label3) {
19623
+ const plain = label3.replace(/[\p{Extended_Pictographic}\u200D\uFE0F]/gu, "").replace(/\bNode9\b/g, "node9").replace(/\s+/g, " ").trim();
19624
+ return plain || "Security Policy";
19625
+ }
19626
+ function asApprovalRequest(description) {
19627
+ const is = description.lastIndexOf(BLOCKED_BY);
19628
+ if (is > 0) {
19629
+ const by = description.slice(is + BLOCKED_BY.length).replace(/\.$/, "");
19630
+ if (by.endsWith(SHIELD)) return `${description.slice(0, is)} needs your approval (${by})`;
19631
+ }
19632
+ const dash = description.lastIndexOf(DASH_BLOCKED_BY);
19633
+ if (dash > 0) {
19634
+ const by = description.slice(dash + DASH_BLOCKED_BY.length).replace(/\.$/, "");
19635
+ if (by.endsWith(SHIELD)) return `${description.slice(0, dash)}. Needs your approval (${by})`;
19636
+ }
19637
+ return description;
19638
+ }
19639
+ var BLOCKED_BY, DASH_BLOCKED_BY, SHIELD;
19640
+ var init_negotiation = __esm({
19641
+ "src/policy/negotiation.ts"() {
19642
+ "use strict";
19643
+ BLOCKED_BY = " is blocked by ";
19644
+ DASH_BLOCKED_BY = " \u2014 blocked by ";
19645
+ SHIELD = " shield";
19646
+ }
19647
+ });
19648
+
19649
+ // src/auth/decision-sources.ts
19650
+ function backgroundPassSource(result) {
19651
+ if (result.checkedBy === "cloud" || result.blockedBy === "team-policy") return "cloud";
19652
+ if (result.blockedBy === "timeout") return "timeout";
19653
+ return "policy";
19654
+ }
19655
+ var HUMAN_DECISION_SOURCES, REDIRECT_SOURCE, DECISION_ROUTE_SOURCES;
19656
+ var init_decision_sources = __esm({
19657
+ "src/auth/decision-sources.ts"() {
19658
+ "use strict";
19659
+ HUMAN_DECISION_SOURCES = /* @__PURE__ */ new Set([
19660
+ "terminal",
19661
+ "browser",
19662
+ "native"
19663
+ ]);
19664
+ REDIRECT_SOURCE = "terminal-redirect";
19665
+ DECISION_ROUTE_SOURCES = /* @__PURE__ */ new Set([
19666
+ ...HUMAN_DECISION_SOURCES,
19667
+ REDIRECT_SOURCE
19668
+ ]);
18623
19669
  }
18624
19670
  });
18625
19671
 
@@ -18739,6 +19785,10 @@ async function authorizeHeadless(toolName, args, meta, options) {
18739
19785
  }
18740
19786
  return _authorizeHeadlessCore(toolName, args, meta, options);
18741
19787
  }
19788
+ function daemonWaitCapMs(approvalTimeoutMs) {
19789
+ if (approvalTimeoutMs === 0) return 0;
19790
+ return Math.max(approvalTimeoutMs ?? 0, 12e4) + 5e3;
19791
+ }
18742
19792
  function approvalTimeoutReason(approvalTimeoutMs) {
18743
19793
  return `No human response within ${approvalTimeoutMs / 1e3}s \u2014 auto-denied by timeout policy. Approve from your phone next time: run \`node9 login\`.`;
18744
19794
  }
@@ -18774,6 +19824,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18774
19824
  let statefulRecoveryCommand;
18775
19825
  let localSmartRuleMatched = false;
18776
19826
  let hardBlockDowngraded = false;
19827
+ let downgradedBlock = null;
18777
19828
  let argScan;
18778
19829
  const argumentScan = () => argScan ??= scanArgumentSecrets(args);
18779
19830
  let fileAccess;
@@ -19393,6 +20444,16 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19393
20444
  humanApproverReachable,
19394
20445
  overridable: policyResult.overridable
19395
20446
  });
20447
+ const blocked = {
20448
+ approved: false,
20449
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
20450
+ blockedBy: "local-config",
20451
+ blockedByLabel: policyResult.blockedByLabel,
20452
+ ruleHit: policyResult.ruleName,
20453
+ ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
20454
+ ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription },
20455
+ ...protectedPathFields(policyResult.ruleName, toolName, args, dlpReviewFlagged)
20456
+ };
19396
20457
  const hardBlock = () => {
19397
20458
  if (!isManual)
19398
20459
  appendLocalAudit(
@@ -19403,17 +20464,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19403
20464
  { ...meta, ruleName: policyResult.ruleName },
19404
20465
  hashAuditArgs
19405
20466
  );
19406
- return {
19407
- approved: false,
19408
- reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
19409
- blockedBy: "local-config",
19410
- blockedByLabel: policyResult.blockedByLabel,
19411
- ruleHit: policyResult.ruleName,
19412
- ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
19413
- ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription },
19414
- ...protectedPathFields(policyResult.ruleName, toolName, args, dlpReviewFlagged)
19415
- };
20467
+ return blocked;
19416
20468
  };
20469
+ downgradedBlock = blocked;
19417
20470
  if (policyResult.dependsOnStatePredicates?.length) {
19418
20471
  const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
19419
20472
  const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
@@ -19470,7 +20523,8 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19470
20523
  policyMatchedWord,
19471
20524
  policyResult.ruleName
19472
20525
  );
19473
- if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
20526
+ if (policyRuleDescription)
20527
+ riskMetadata.ruleDescription = (hardBlockDowngraded ? asApprovalRequest(policyRuleDescription) : policyRuleDescription).slice(0, 200);
19474
20528
  const persistent = policyResult.ruleName || policyResult.tier === 7 || hardBlockDowngraded ? null : getPersistentDecision(toolName);
19475
20529
  if (persistent === "allow" && !callSpecificReview()) {
19476
20530
  if (!isManual)
@@ -19634,7 +20688,7 @@ ${packageReview}`;
19634
20688
  }
19635
20689
  }
19636
20690
  if (initResult.pending) cloudRequestId = initResult.requestId || null;
19637
- if (!callSpecificReview()) explainableLabel = "Organization Policy (SaaS)";
20691
+ if (!reviewDemanded()) explainableLabel = "Organization Policy (SaaS)";
19638
20692
  } catch {
19639
20693
  }
19640
20694
  }
@@ -19776,10 +20830,54 @@ ${packageReview}`;
19776
20830
  decision: daemonDecision,
19777
20831
  source: decisionSource,
19778
20832
  reason: daemonReason
19779
- } = await waitForDaemonDecision(daemonEntryId, signal);
20833
+ } = await waitForDaemonDecision(
20834
+ daemonEntryId,
20835
+ signal,
20836
+ daemonWaitCapMs(config.settings.approvalTimeoutMs)
20837
+ );
19780
20838
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
19781
20839
  const isApproved = daemonDecision === "allow";
19782
- const isRedirect = decisionSource === "terminal-redirect";
20840
+ const isRedirect = decisionSource === REDIRECT_SOURCE;
20841
+ if (!isApproved && !isRedirect && !(decisionSource && HUMAN_DECISION_SOURCES.has(decisionSource))) {
20842
+ if (decisionSource === "cloud")
20843
+ return {
20844
+ approved: false,
20845
+ reason: daemonReason || "Action rejected by organization administrator via Slack.",
20846
+ blockedBy: "team-policy",
20847
+ blockedByLabel: "Organization Policy (SaaS)",
20848
+ decisionSource: "cloud"
20849
+ };
20850
+ const timedOut = decisionSource === "timeout";
20851
+ if (downgradedBlock)
20852
+ return {
20853
+ ...downgradedBlock,
20854
+ blockedBy: timedOut ? "timeout" : "local-config"
20855
+ };
20856
+ if (timedOut)
20857
+ return {
20858
+ approved: false,
20859
+ reason: daemonReason || "No response: auto-denied after the approval timeout.",
20860
+ blockedBy: "timeout",
20861
+ blockedByLabel: "Approval Timeout"
20862
+ };
20863
+ if (decisionSource === "policy")
20864
+ return {
20865
+ approved: false,
20866
+ reason: daemonReason || "Action blocked by policy.",
20867
+ blockedBy: "local-config",
20868
+ blockedByLabel: explainableLabel
20869
+ };
20870
+ return {
20871
+ approved: false,
20872
+ reason: daemonReason || "node9 could not get an approval decision for this call.",
20873
+ blockedBy: "no-approval-mechanism",
20874
+ blockedByLabel: "Approval Unavailable"
20875
+ };
20876
+ }
20877
+ if (isApproved && decisionSource === "cloud")
20878
+ return { approved: true, checkedBy: "cloud", decisionSource: "cloud" };
20879
+ if (isApproved && decisionSource === "policy")
20880
+ return { approved: true, checkedBy: "local-policy" };
19783
20881
  const via = "Terminal (node9 tail)";
19784
20882
  return {
19785
20883
  approved: isApproved,
@@ -19937,6 +21035,8 @@ var init_orchestrator = __esm({
19937
21035
  init_shields();
19938
21036
  init_jail();
19939
21037
  init_safe_text();
21038
+ init_negotiation();
21039
+ init_decision_sources();
19940
21040
  SENSITIVE_PATH_PATTERN = sensitivePathMatch("").patternName;
19941
21041
  WRITE_TOOLS = /* @__PURE__ */ new Set([
19942
21042
  "write",
@@ -30939,23 +32039,22 @@ function checkApprovalConfig(ctx) {
30939
32039
  const s = getConfig(ctx.cwd).settings;
30940
32040
  const a = s.approvers ?? { native: false, browser: false, cloud: false, terminal: false };
30941
32041
  const hasChannel = a.native || a.browser || a.cloud || a.terminal || s.reviewChannel === "ask";
30942
- if (s.approvalTimeoutMs === 0) {
30943
- return [
30944
- {
30945
- category: "Approvals",
30946
- severity: "high",
30947
- title: "Approval timeout is 0 \u2014 every review auto-denies instantly",
30948
- what: "A review request is denied the moment it is raised; no human ever sees it.",
30949
- why: "`settings.approvalTimeoutMs` is 0.",
30950
- who: "Legitimate actions that need a human OK are silently blocked.",
30951
- detail: [],
30952
- owner: "node9",
30953
- fix: "Set a real approval timeout (e.g. 300000 ms) or clear it to use the default."
30954
- }
30955
- ];
30956
- }
32042
+ const zeroTimeout = s.approvalTimeoutMs === 0 ? [
32043
+ {
32044
+ category: "Approvals",
32045
+ severity: "medium",
32046
+ title: "Approval timeout is 0: node9 never times a review out",
32047
+ what: "A review waits for a person; node9's own timer is off.",
32048
+ why: "`settings.approvalTimeoutMs` is 0.",
32049
+ who: "With no one at the screen, the agent's hook timeout ends the review, and some agents then run the tool.",
32050
+ detail: [],
32051
+ owner: "node9",
32052
+ fix: "Set a real approval timeout (e.g. 300000 ms) or clear it to use the default."
32053
+ }
32054
+ ] : [];
30957
32055
  if (!hasChannel) {
30958
32056
  return [
32057
+ ...zeroTimeout,
30959
32058
  {
30960
32059
  category: "Approvals",
30961
32060
  severity: "high",
@@ -30969,7 +32068,7 @@ function checkApprovalConfig(ctx) {
30969
32068
  }
30970
32069
  ];
30971
32070
  }
30972
- return [];
32071
+ return zeroTimeout;
30973
32072
  }
30974
32073
  function checkToolGovernance(ctx) {
30975
32074
  const cfg = getConfig(ctx.cwd);
@@ -33651,7 +34750,17 @@ var init_osv_sync = __esm({
33651
34750
  init_zip();
33652
34751
  init_osv_index();
33653
34752
  init_net();
33654
- ECOSYSTEMS2 = ["npm", "PyPI"];
34753
+ ECOSYSTEMS2 = [
34754
+ "npm",
34755
+ "PyPI",
34756
+ "RubyGems",
34757
+ "crates.io",
34758
+ "Go",
34759
+ "NuGet",
34760
+ "Maven",
34761
+ "Packagist",
34762
+ "VSCode"
34763
+ ];
33655
34764
  SYNC_INTERVAL_MS2 = 6 * 60 * 60 * 1e3;
33656
34765
  FIRST_SYNC_DELAY_MS = 2 * 60 * 1e3;
33657
34766
  FULL_REBUILD_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -34211,6 +35320,15 @@ function buildDaemonReport(allEntries, period, now) {
34211
35320
  byAgent: [...agentMap.values()].sort((a, b) => b.total - a.total)
34212
35321
  };
34213
35322
  }
35323
+ function autoDenyMs() {
35324
+ const ms = getConfig().settings.approvalTimeoutMs;
35325
+ if (ms === 0) return null;
35326
+ return ms ?? AUTO_DENY_MS;
35327
+ }
35328
+ function armAutoDeny(fire) {
35329
+ const ms = autoDenyMs();
35330
+ return ms === null ? void 0 : setTimeout(fire, ms);
35331
+ }
34214
35332
  function startDaemon() {
34215
35333
  try {
34216
35334
  autoMigrateLocalConfig();
@@ -34321,7 +35439,7 @@ data: ${JSON.stringify({
34321
35439
  allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
34322
35440
  })),
34323
35441
  orgName: getOrgName(),
34324
- autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
35442
+ autoDenyMs: autoDenyMs()
34325
35443
  })}
34326
35444
 
34327
35445
  `
@@ -34384,7 +35502,7 @@ data: ${JSON.stringify(item.data)}
34384
35502
  timestamp: Date.now(),
34385
35503
  earlyDecision: null,
34386
35504
  waiter: null,
34387
- timer: setTimeout(() => {
35505
+ timer: armAutoDeny(() => {
34388
35506
  if (pending.has(id)) {
34389
35507
  const e = pending.get(id);
34390
35508
  appendAuditLog({
@@ -34392,6 +35510,7 @@ data: ${JSON.stringify(item.data)}
34392
35510
  args: e.args,
34393
35511
  decision: "auto-deny"
34394
35512
  });
35513
+ e.decisionSource = "timeout";
34395
35514
  if (e.waiter) e.waiter("deny", "No response \u2014 auto-denied after timeout");
34396
35515
  else {
34397
35516
  e.earlyDecision = "deny";
@@ -34400,7 +35519,7 @@ data: ${JSON.stringify(item.data)}
34400
35519
  pending.delete(id);
34401
35520
  broadcast("remove", { id, decision: "deny" });
34402
35521
  }
34403
- }, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
35522
+ })
34404
35523
  };
34405
35524
  pending.set(id, entry);
34406
35525
  if (!fromCLI) {
@@ -34462,6 +35581,7 @@ data: ${JSON.stringify(item.data)}
34462
35581
  clearTimeout(e.timer);
34463
35582
  const decision = result.approved ? "allow" : "deny";
34464
35583
  appendAuditLog({ toolName: e.toolName, args: e.args, decision });
35584
+ e.decisionSource = backgroundPassSource(result);
34465
35585
  if (e.waiter) {
34466
35586
  e.waiter(decision, result.reason);
34467
35587
  pending.delete(id);
@@ -34576,8 +35696,7 @@ data: ${JSON.stringify(item.data)}
34576
35696
  insightCounts.delete(entry.toolName);
34577
35697
  saveInsightCounts();
34578
35698
  }
34579
- const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native", "terminal-redirect"]);
34580
- if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
35699
+ if (source && DECISION_ROUTE_SOURCES.has(source)) entry.decisionSource = source;
34581
35700
  if (entry.waiter) {
34582
35701
  entry.waiter(resolvedDecision, reason);
34583
35702
  pending.delete(id);
@@ -34715,8 +35834,7 @@ data: ${JSON.stringify(item.data)}
34715
35834
  insightCounts.delete(entry.toolName);
34716
35835
  saveInsightCounts();
34717
35836
  }
34718
- const VALID_RESOLVE_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
34719
- if (source && VALID_RESOLVE_SOURCES.has(source)) entry.decisionSource = source;
35837
+ if (source && HUMAN_DECISION_SOURCES.has(source)) entry.decisionSource = source;
34720
35838
  if (entry.waiter) entry.waiter(resolvedResolveDecision);
34721
35839
  else entry.earlyDecision = resolvedResolveDecision;
34722
35840
  pending.delete(id);
@@ -35374,6 +36492,7 @@ var init_server = __esm({
35374
36492
  "use strict";
35375
36493
  init_migrate();
35376
36494
  init_trusted_recipients();
36495
+ init_decision_sources();
35377
36496
  init_core();
35378
36497
  init_keyed_guard();
35379
36498
  init_scan();
@@ -35632,14 +36751,14 @@ async function onboardMachine(apiKey, opts = {}) {
35632
36751
  detail: `${sync.rules} cloud rule${sync.rules === 1 ? "" : "s"} active`
35633
36752
  } : { name: "policy-sync", ok: false, detail: sync.reason }
35634
36753
  );
35635
- const push = await runPolicyPush();
36754
+ const push2 = await runPolicyPush();
35636
36755
  steps.push(
35637
- push.ok ? { name: "register", ok: true, detail: "machine visible in the dashboard" } : { name: "register", ok: false, detail: push.reason }
36756
+ push2.ok ? { name: "register", ok: true, detail: "machine visible in the dashboard" } : { name: "register", ok: false, detail: push2.reason }
35638
36757
  );
35639
36758
  const healed = ensureAutostartHealthy(!!getConfig().settings.autoStartDaemon);
35640
36759
  let detail = healed === "repaired" ? "autostart re-enabled (survives reboot)" : isDaemonRunning() ? "running" : healed === "unsupported" ? "no background service on this platform: starts on agent activity" : "starts on agent activity (make it survive reboots: node9 daemon install)";
35641
36760
  let daemonOk = true;
35642
- if (sync.ok && push.ok && isInteractive() && mayChangeService({ stdoutTTY: !!process.stdout.isTTY, ci: isCI() }) && autostartState() === "absent") {
36761
+ if (sync.ok && push2.ok && isInteractive() && mayChangeService({ stdoutTTY: !!process.stdout.isTTY, ci: isCI() }) && autostartState() === "absent") {
35643
36762
  try {
35644
36763
  if (await confirm2({
35645
36764
  message: "Run node9 in the background when this computer starts?",
@@ -63906,108 +65025,14 @@ function parseDuration(str2) {
63906
65025
 
63907
65026
  // src/proxy/index.ts
63908
65027
  init_orchestrator();
65028
+ init_negotiation();
65029
+ init_platform_shell();
65030
+ init_safe_text();
63909
65031
  import readline2 from "readline";
63910
65032
  import chalk9 from "chalk";
63911
65033
  import { spawn as spawn3 } from "child_process";
63912
65034
  import { execa } from "execa";
63913
65035
  import { parseCommandString } from "execa";
63914
-
63915
- // src/policy/negotiation.ts
63916
- function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason, recoveryCommand, context) {
63917
- if (isHumanDecision) {
63918
- return `NODE9: The human user rejected this action.
63919
- REASON: ${humanReason || "No specific reason provided."}
63920
- INSTRUCTIONS:
63921
- - Do NOT retry this exact command.
63922
- - Acknowledge the block to the user and ask if there is an alternative approach.
63923
- - If you believe this action is critical, explain your reasoning and ask them to run "node9 pause 15m" to proceed.`;
63924
- }
63925
- if (context?.kind === "protected-path") {
63926
- const subject = context.path ? `${context.path} is a protected file` : "This file is protected";
63927
- return `NODE9: Blocked. ${subject}, and node9 stopped this call to it. The call did not run, so nothing was exposed.
63928
- INSTRUCTIONS:
63929
- - Do NOT retry, and do NOT reach the file another way (cat, grep, a copy, a script, a redirect).
63930
- - If this step needs that file, ask the user to provide what you need or to do the step themselves.
63931
- - Tell the user node9 protected this file, then ask how to proceed.`;
63932
- }
63933
- const label3 = blockedByLabel.toLowerCase();
63934
- if (label3.includes("dlp") || label3.includes("secret detected") || label3.includes("credential review")) {
63935
- return `NODE9 SECURITY ALERT: A sensitive credential (API key, token, or private key) was found in your tool call arguments.
63936
- CRITICAL INSTRUCTION: Do NOT retry this action.
63937
- REQUIRED ACTIONS:
63938
- 1. Remove the hardcoded credential from your command or code.
63939
- 2. Use an environment variable or a dedicated secrets manager instead.
63940
- 3. Treat the leaked credential as compromised and rotate it immediately.
63941
- Do NOT attempt to bypass this check or pass the credential through another tool.
63942
- - Tell the user node9 blocked this and why, then ask how to proceed.`;
63943
- }
63944
- if (label3.includes("package check")) {
63945
- return `NODE9: Blocked \u2014 ${humanReason || "a package in this command is known to be malicious."}
63946
- INSTRUCTIONS:
63947
- - Do NOT install this package, and do NOT fetch it another way (a different package manager, a tarball URL, a git URL, a mirror).
63948
- - If a dependency is needed, suggest a well-known alternative and let the user choose.
63949
- - Tell the user node9 blocked this install and name the advisory, then ask how to proceed.`;
63950
- }
63951
- if (label3.includes("sql safety") && label3.includes("delete without where")) {
63952
- return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
63953
- INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
63954
- Do NOT retry without a WHERE clause.
63955
- - Tell the user node9 blocked this and why, then ask how to proceed.`;
63956
- }
63957
- if (label3.includes("sql safety") && label3.includes("update without where")) {
63958
- return `NODE9: Blocked \u2014 UPDATE without WHERE clause would update every row.
63959
- INSTRUCTION: Add a WHERE clause to scope the update (e.g. WHERE id = <value>).
63960
- Do NOT retry without a WHERE clause.
63961
- - Tell the user node9 blocked this and why, then ask how to proceed.`;
63962
- }
63963
- if (label3.includes("dangerous word")) {
63964
- const match = blockedByLabel.match(/dangerous word: "([^"]+)"/i);
63965
- const word = match?.[1] ?? "a dangerous keyword";
63966
- return `NODE9: Blocked \u2014 command contains forbidden keyword "${word}".
63967
- INSTRUCTION: Do NOT use "${word}". Use a non-destructive alternative.
63968
- Do NOT attempt to bypass this with shell tricks or aliases \u2014 it will be blocked again.
63969
- - Tell the user node9 blocked this and why, then ask how to proceed.`;
63970
- }
63971
- if (label3.includes("path blocked") || label3.includes("sandbox")) {
63972
- return `NODE9: Blocked \u2014 operation targets a path outside the allowed sandbox.
63973
- INSTRUCTION: Move your output to an allowed directory such as /tmp/ or the project directory.
63974
- Do NOT retry on the same path.
63975
- - Tell the user node9 blocked this and why, then ask how to proceed.`;
63976
- }
63977
- if (label3.includes("inline execution")) {
63978
- return `NODE9: Blocked \u2014 inline code execution (e.g. bash -c "...") is not allowed.
63979
- INSTRUCTION: Use individual tool calls instead of embedding code in a shell string.
63980
- - Tell the user node9 blocked this and why, then ask how to proceed.`;
63981
- }
63982
- if (label3.includes("strict mode")) {
63983
- return `NODE9: Blocked \u2014 strict mode is active. All tool calls require explicit human approval.
63984
- INSTRUCTION: Inform the user this action is pending approval. Wait for them to approve via the dashboard or run "node9 pause".`;
63985
- }
63986
- if (label3.includes("rule") && label3.includes("default block")) {
63987
- const match = blockedByLabel.match(/rule "([^"]+)"/i);
63988
- const rule = match?.[1] ?? "a policy rule";
63989
- return `NODE9: Blocked \u2014 action "${rule}" is forbidden by security policy.
63990
- INSTRUCTION: Do NOT use "${rule}". Find a read-only or non-destructive alternative.
63991
- Do NOT attempt to bypass this rule.
63992
- - Tell the user node9 blocked this and why, then ask how to proceed.`;
63993
- }
63994
- const recovery = recoveryCommand ? `
63995
- REQUIRED ACTION: Run \`${recoveryCommand}\` first, then retry your original command.` : "\n- Pivot to a non-destructive or read-only alternative.";
63996
- return `NODE9: Action blocked by security policy [${blockedByLabel}].
63997
- INSTRUCTIONS:
63998
- - Do NOT retry this exact command or attempt to bypass the rule.${recovery}
63999
- - Inform the user which security rule was triggered and ask how to proceed.`;
64000
- }
64001
- function buildReviewMessage(blockedByLabel, ruleDescription, reason) {
64002
- const why = ruleDescription || (reason && reason !== blockedByLabel ? reason : void 0) || blockedByLabel || "this action needs your review";
64003
- const trimmed = why.trimEnd();
64004
- const sentence2 = /[.!?\u2026]$/.test(trimmed) ? trimmed : `${trimmed}.`;
64005
- return `Node9 flagged this for your review: ${sentence2} Approve to proceed, or deny to cancel.`;
64006
- }
64007
-
64008
- // src/proxy/index.ts
64009
- init_platform_shell();
64010
- init_safe_text();
64011
65036
  async function runProxy(targetCommand) {
64012
65037
  const commandParts = parseCommandString(targetCommand);
64013
65038
  const cmd = commandParts[0];
@@ -64053,7 +65078,7 @@ async function runProxy(targetCommand) {
64053
65078
  console.error(chalk9.gray(` Reason: ${result.reason || "Security Policy"}
64054
65079
  `));
64055
65080
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
64056
- const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
65081
+ const isHuman = isHumanDecision(blockedByLabel, result.blockedBy);
64057
65082
  const aiInstruction = buildNegotiationMessage(
64058
65083
  blockedByLabel,
64059
65084
  isHuman,
@@ -64184,12 +65209,13 @@ init_daemon();
64184
65209
  init_dist();
64185
65210
  init_startup_log();
64186
65211
  init_config();
65212
+ init_negotiation();
65213
+ init_daemon_starter();
64187
65214
  import chalk13 from "chalk";
64188
65215
  import fs70 from "fs";
64189
65216
  import { spawn as spawn6 } from "child_process";
64190
65217
  import path67 from "path";
64191
65218
  import os59 from "os";
64192
- init_daemon_starter();
64193
65219
 
64194
65220
  // src/skill-pin.ts
64195
65221
  init_read_capped();
@@ -64734,7 +65760,7 @@ RAW: ${raw}
64734
65760
  const mcpServer = mcpMatch?.[1];
64735
65761
  const sendBlock = (msg, result2) => {
64736
65762
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || (isKeyedForPolicy() ? "Workspace Policy" : "Local Security Policy");
64737
- const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
65763
+ const humanDecision = isHumanDecision(blockedByContext, result2?.blockedBy);
64738
65764
  let ttyFd = null;
64739
65765
  try {
64740
65766
  ttyFd = fs70.openSync("/dev/tty", "w");
@@ -64771,7 +65797,7 @@ RAW: ${raw}
64771
65797
  }
64772
65798
  const aiFeedbackMessage = buildNegotiationMessage(
64773
65799
  blockedByContext,
64774
- isHumanDecision,
65800
+ humanDecision,
64775
65801
  msg,
64776
65802
  result2?.recoveryCommand,
64777
65803
  { kind: result2?.blockKind, path: result2?.blockedPath }
@@ -65179,7 +66205,7 @@ function collectToolOutputText(toolResponse, opts = {}) {
65179
66205
  let size = 0;
65180
66206
  let fields = 0;
65181
66207
  let truncated = false;
65182
- const push = (s) => {
66208
+ const push2 = (s) => {
65183
66209
  if (s.length === 0) return true;
65184
66210
  const room = maxBytes - size;
65185
66211
  if (s.length > room) {
@@ -65197,7 +66223,7 @@ function collectToolOutputText(toolResponse, opts = {}) {
65197
66223
  return true;
65198
66224
  };
65199
66225
  const walk2 = (v, depth) => {
65200
- if (typeof v === "string") return push(v);
66226
+ if (typeof v === "string") return push2(v);
65201
66227
  if (depth >= MAX_DEPTH5 || v === null || typeof v !== "object") return true;
65202
66228
  const children = Array.isArray(v) ? v : Object.values(v);
65203
66229
  for (const child of children) if (!walk2(child, depth + 1)) return false;
@@ -68359,17 +69385,18 @@ init_orchestrator();
68359
69385
  init_config();
68360
69386
  init_cloud();
68361
69387
  init_config();
68362
- import readline3 from "readline";
68363
- import chalk24 from "chalk";
68364
- import { spawn as spawn8 } from "child_process";
68365
- import { randomUUID as randomUUID8 } from "crypto";
68366
- import { execa as execa2 } from "execa";
69388
+ init_negotiation();
68367
69389
  init_provenance();
68368
69390
  init_mcp_pin();
68369
69391
  init_mcp_cmd();
68370
69392
  init_mcp_tools();
68371
69393
  init_daemon();
68372
69394
  init_safe_text();
69395
+ import readline3 from "readline";
69396
+ import chalk24 from "chalk";
69397
+ import { spawn as spawn8 } from "child_process";
69398
+ import { randomUUID as randomUUID8 } from "crypto";
69399
+ import { execa as execa2 } from "execa";
68373
69400
 
68374
69401
  // src/mcp-gateway/result-scan.ts
68375
69402
  init_dlp();
@@ -68733,10 +69760,10 @@ async function runMcpGateway(upstreamCommand, configName) {
68733
69760
  console.error(chalk24.gray(` Reason: ${result.reason ?? "Security Policy"}
68734
69761
  `));
68735
69762
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
68736
- const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
69763
+ const humanDecision = isHumanDecision(blockedByLabel, result.blockedBy);
68737
69764
  const aiInstruction = buildNegotiationMessage(
68738
69765
  blockedByLabel,
68739
- isHumanDecision,
69766
+ humanDecision,
68740
69767
  result.reason,
68741
69768
  void 0,
68742
69769
  { kind: result.blockKind, path: result.blockedPath }
@@ -75823,7 +76850,7 @@ function addHost(list, host2) {
75823
76850
  if (!cliGuardPolicyWrite(`egress ${list} ${host2}`)) return false;
75824
76851
  return guard(() => addEgressHost(list, host2));
75825
76852
  }
75826
- function exempt(address) {
76853
+ function exempt2(address) {
75827
76854
  if (!cliGuardPolicyWrite(`egress exempt ${address}`)) return false;
75828
76855
  return guard(() => addSsrfExemption(address));
75829
76856
  }
@@ -76006,7 +77033,7 @@ function registerEgressCommand(program2) {
76006
77033
  process.exitCode = 1;
76007
77034
  return;
76008
77035
  }
76009
- if (!exempt(a)) return;
77036
+ if (!exempt2(a)) return;
76010
77037
  console.log(chalk34.green(`
76011
77038
  \u2713 ${a} is exempt from the floor.`));
76012
77039
  if (!m)
@@ -77795,17 +78822,18 @@ function registerSessionExposureCommand(program2) {
77795
78822
  init_osv_index();
77796
78823
  init_osv_sync();
77797
78824
  import chalk42 from "chalk";
78825
+ var ECO_WIDTH = Math.max(...ECOSYSTEMS2.map((e) => e.length));
77798
78826
  function registerPackageIndexCommand(program2) {
77799
78827
  const cmd = program2.command("package-index").description("Local OSV malicious-package index used to check installs before they run");
77800
78828
  cmd.command("status").description("Show when each ecosystem was last synced and how many records it holds").action(() => {
77801
78829
  for (const eco of ECOSYSTEMS2) {
77802
78830
  const meta = readMeta(eco);
77803
78831
  if (!meta) {
77804
- console.log(`${eco.padEnd(5)} ${chalk42.yellow("not synced")}`);
78832
+ console.log(`${eco.padEnd(ECO_WIDTH)} ${chalk42.yellow("not synced")}`);
77805
78833
  continue;
77806
78834
  }
77807
78835
  console.log(
77808
- `${eco.padEnd(5)} ${meta.records} records synced ${meta.syncedAt} (${meta.mode})`
78836
+ `${eco.padEnd(ECO_WIDTH)} ${meta.records} records synced ${meta.syncedAt} (${meta.mode})`
77809
78837
  );
77810
78838
  }
77811
78839
  });
@@ -77815,9 +78843,9 @@ function registerPackageIndexCommand(program2) {
77815
78843
  for (const r of results) {
77816
78844
  if (r.error) {
77817
78845
  failed = true;
77818
- console.error(`${r.ecosystem.padEnd(5)} ${chalk42.red("failed")}: ${r.error}`);
78846
+ console.error(`${r.ecosystem.padEnd(ECO_WIDTH)} ${chalk42.red("failed")}: ${r.error}`);
77819
78847
  } else {
77820
- console.log(`${r.ecosystem.padEnd(5)} ${r.mode}: ${r.records} records`);
78848
+ console.log(`${r.ecosystem.padEnd(ECO_WIDTH)} ${r.mode}: ${r.records} records`);
77821
78849
  }
77822
78850
  }
77823
78851
  process.exitCode = failed ? 1 : 0;