@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.js CHANGED
@@ -1193,6 +1193,120 @@ function expandPlainParam(p) {
1193
1193
  if (recorded !== void 0) return recorded;
1194
1194
  return HOME_VARIABLES.has(p.Param?.Value) ? "~" : void 0;
1195
1195
  }
1196
+ function functionBodies(f) {
1197
+ const out = /* @__PURE__ */ new Map();
1198
+ syntax.Walk(f, (node) => {
1199
+ const n = node;
1200
+ if (n && syntax.NodeType(n) === "FuncDecl" && n.Name?.Value && n.Body) {
1201
+ const list = out.get(n.Name.Value) ?? [];
1202
+ list.push(n.Body);
1203
+ out.set(n.Name.Value, list);
1204
+ }
1205
+ return true;
1206
+ });
1207
+ return out;
1208
+ }
1209
+ function rebindsPositionals(body) {
1210
+ let found = false;
1211
+ syntax.Walk(body, (node) => {
1212
+ const n = node;
1213
+ if (found || !n) return false;
1214
+ if (syntax.NodeType(n) === "CallExpr") {
1215
+ const words = (n.Args || []).map((a) => resolveWordLiteral(a));
1216
+ if (words[0] === "shift") found = true;
1217
+ else if (words[0] === "set" && words.slice(1).some((w) => w === null || w === "--" || !/^[-+]/.test(w)))
1218
+ found = true;
1219
+ }
1220
+ return !found;
1221
+ });
1222
+ return found;
1223
+ }
1224
+ function tableBindings() {
1225
+ if (!assignmentTable) return "";
1226
+ const out = [];
1227
+ for (const [name, rec] of assignmentTable)
1228
+ if (rec.at < currentStmtOffset) out.push(`${name}=${rec.value ?? "\0"}`);
1229
+ return out.sort().join("");
1230
+ }
1231
+ function walkWithCalls(f, visit2) {
1232
+ const bodies = functionBodies(f);
1233
+ if (bodies.size === 0) {
1234
+ syntax.Walk(f, visit2);
1235
+ return;
1236
+ }
1237
+ const seenCalls = /* @__PURE__ */ new Set();
1238
+ const inline = (call) => {
1239
+ const words = (call.Args || []).map((a) => resolveWordLiteral(a));
1240
+ const list = words[0] ? bodies.get(words[0]) : void 0;
1241
+ if (!list || callDepth >= MAX_CALL_DEPTH) return;
1242
+ const args = words.slice(1);
1243
+ const key = `${words[0]}\0${JSON.stringify(args)}\0${tableBindings()}`;
1244
+ if (seenCalls.has(key)) return;
1245
+ seenCalls.add(key);
1246
+ const outerTable = assignmentTable;
1247
+ const outerOffset = currentStmtOffset;
1248
+ for (const body of list) {
1249
+ if (callWalks >= MAX_CALL_WALKS) return;
1250
+ callWalks++;
1251
+ const table = /* @__PURE__ */ new Map();
1252
+ for (const [name, rec] of outerTable ?? [])
1253
+ if (rec.at < outerOffset) table.set(name, { value: rec.value, at: -1 });
1254
+ for (const name of ["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*", "#"])
1255
+ table.delete(name);
1256
+ if (!rebindsPositionals(body)) {
1257
+ args.slice(0, 9).forEach((value, i) => table.set(String(i + 1), { value, at: -1 }));
1258
+ if (args.length === 1) {
1259
+ table.set("@", { value: args[0], at: -1 });
1260
+ table.set("*", { value: args[0], at: -1 });
1261
+ }
1262
+ table.set("#", { value: String(args.length), at: -1 });
1263
+ }
1264
+ assignmentTable = table;
1265
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1266
+ callDepth++;
1267
+ try {
1268
+ if (body.Cmd && syntax.NodeType(body.Cmd) === "Block") recordTopLevelAssignments(body.Cmd);
1269
+ syntax.Walk(body, step);
1270
+ } finally {
1271
+ callDepth--;
1272
+ assignmentTable = outerTable;
1273
+ currentStmtOffset = outerOffset;
1274
+ }
1275
+ }
1276
+ };
1277
+ const step = (node) => {
1278
+ if (!node) return false;
1279
+ const go = visit2(node);
1280
+ if (go !== false && syntax.NodeType(node) === "CallExpr") inline(node);
1281
+ return go;
1282
+ };
1283
+ syntax.Walk(f, step);
1284
+ }
1285
+ function withAssignments(f, body) {
1286
+ const outerTable = assignmentTable;
1287
+ const outerOffset = currentStmtOffset;
1288
+ const outerWalks = callWalks;
1289
+ callWalks = 0;
1290
+ assignmentTable = new Map(
1291
+ [...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
1292
+ );
1293
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1294
+ try {
1295
+ recordTopLevelAssignments(f);
1296
+ return body();
1297
+ } finally {
1298
+ assignmentTable = outerTable;
1299
+ currentStmtOffset = outerOffset;
1300
+ callWalks = outerWalks;
1301
+ }
1302
+ }
1303
+ function trackStatement(n) {
1304
+ try {
1305
+ currentStmtOffset = n.Pos().Offset();
1306
+ } catch {
1307
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1308
+ }
1309
+ }
1196
1310
  function resolveWordLiteral(w) {
1197
1311
  const parts = w?.Parts || [];
1198
1312
  let s = "";
@@ -1313,27 +1427,31 @@ function extractShellDestinations(command) {
1313
1427
  const out = [];
1314
1428
  const seen = /* @__PURE__ */ new Set();
1315
1429
  try {
1316
- syntax.Walk(f, (node) => {
1317
- if (!node) return false;
1318
- const n = node;
1319
- if (syntax.NodeType(n) !== "CallExpr") return true;
1320
- const callArgs = n.Args || [];
1321
- if (callArgs.length === 0) return true;
1322
- const words = callArgs.map((a) => resolveWordLiteral(a));
1323
- const at = unwrapCommandHead(words);
1324
- const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
1325
- if (!NET_BINARIES.has(name)) return true;
1326
- const rest = words.slice(at + 1);
1327
- for (const raw of destTokensForBinary(name, rest)) {
1328
- const host2 = parseDestHost(raw);
1329
- if (!host2) continue;
1330
- const key = `${name}:${host2}`;
1331
- if (seen.has(key)) continue;
1332
- seen.add(key);
1333
- out.push({ host: host2, binary: name, raw });
1334
- }
1335
- return true;
1336
- });
1430
+ withAssignments(
1431
+ f,
1432
+ () => walkWithCalls(f, (node) => {
1433
+ if (!node) return false;
1434
+ const n = node;
1435
+ if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1436
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1437
+ const callArgs = n.Args || [];
1438
+ if (callArgs.length === 0) return true;
1439
+ const words = callArgs.map((a) => resolveWordLiteral(a));
1440
+ const at = unwrapCommandHead(words);
1441
+ const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
1442
+ if (!NET_BINARIES.has(name)) return true;
1443
+ const rest = words.slice(at + 1);
1444
+ for (const raw of destTokensForBinary(name, rest)) {
1445
+ const host2 = parseDestHost(raw);
1446
+ if (!host2) continue;
1447
+ const key = `${name}:${host2}`;
1448
+ if (seen.has(key)) continue;
1449
+ seen.add(key);
1450
+ out.push({ host: host2, binary: name, raw });
1451
+ }
1452
+ return true;
1453
+ })
1454
+ );
1337
1455
  } catch {
1338
1456
  return out;
1339
1457
  }
@@ -1345,39 +1463,45 @@ function extractShellDestTokens(command) {
1345
1463
  const out = [];
1346
1464
  const seen = /* @__PURE__ */ new Set();
1347
1465
  try {
1348
- syntax.Walk(f, (node) => {
1349
- if (!node) return false;
1350
- const n = node;
1351
- if (syntax.NodeType(n) !== "CallExpr") return true;
1352
- const callArgs = n.Args || [];
1353
- if (callArgs.length === 0) return true;
1354
- const name = (resolveWordLiteral(callArgs[0]) || "").toLowerCase();
1355
- if (!NET_BINARIES.has(name)) return true;
1356
- const rest = callArgs.slice(1).map((a) => resolveWordLiteral(a));
1357
- for (const raw of destTokensForBinary(name, rest)) {
1358
- if (!raw) continue;
1359
- let tok = raw.trim();
1360
- const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(tok);
1361
- const hasScheme = scheme !== null;
1362
- if (hasScheme) tok = tok.slice(scheme[0].length);
1363
- tok = tok.split(/[/?#]/)[0];
1364
- const at = tok.lastIndexOf("@");
1365
- if (at >= 0) tok = tok.slice(at + 1);
1366
- if (tok.startsWith("[")) {
1367
- const close = tok.indexOf("]");
1368
- if (close > 0) tok = tok.slice(0, close + 1);
1369
- } else {
1370
- tok = tok.split(":")[0];
1466
+ withAssignments(
1467
+ f,
1468
+ () => walkWithCalls(f, (node) => {
1469
+ if (!node) return false;
1470
+ const n = node;
1471
+ if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1472
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1473
+ const callArgs = n.Args || [];
1474
+ if (callArgs.length === 0) return true;
1475
+ const words = callArgs.map((a) => resolveWordLiteral(a));
1476
+ const head = unwrapCommandHead(words);
1477
+ const name = (words[head] || "").toLowerCase().split("/").pop() ?? "";
1478
+ if (!NET_BINARIES.has(name)) return true;
1479
+ const rest = words.slice(head + 1);
1480
+ for (const raw of destTokensForBinary(name, rest)) {
1481
+ if (!raw) continue;
1482
+ let tok = raw.trim();
1483
+ const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(tok);
1484
+ const hasScheme = scheme !== null;
1485
+ if (hasScheme) tok = tok.slice(scheme[0].length);
1486
+ tok = tok.split(/[/?#]/)[0];
1487
+ const at = tok.lastIndexOf("@");
1488
+ if (at >= 0) tok = tok.slice(at + 1);
1489
+ if (tok.startsWith("[")) {
1490
+ const close = tok.indexOf("]");
1491
+ if (close > 0) tok = tok.slice(0, close + 1);
1492
+ } else {
1493
+ tok = tok.split(":")[0];
1494
+ }
1495
+ if (!tok) continue;
1496
+ if (!hasScheme && /^\d+$/.test(tok) && Number(tok) < 16777216) continue;
1497
+ const key = `${name}:${tok}`;
1498
+ if (seen.has(key)) continue;
1499
+ seen.add(key);
1500
+ out.push({ token: tok, binary: name });
1371
1501
  }
1372
- if (!tok) continue;
1373
- if (!hasScheme && /^\d+$/.test(tok) && Number(tok) < 16777216) continue;
1374
- const key = `${name}:${tok}`;
1375
- if (seen.has(key)) continue;
1376
- seen.add(key);
1377
- out.push({ token: tok, binary: name });
1378
- }
1379
- return true;
1380
- });
1502
+ return true;
1503
+ })
1504
+ );
1381
1505
  } catch {
1382
1506
  return out;
1383
1507
  }
@@ -1496,6 +1620,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1496
1620
  if (depth === 0) {
1497
1621
  seenPayloads = /* @__PURE__ */ new Set();
1498
1622
  payloadBudget = PAYLOAD_BUDGET;
1623
+ if (callDepth === 0) callWalks = 0;
1499
1624
  }
1500
1625
  assignmentTable = new Map(
1501
1626
  [...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
@@ -1503,7 +1628,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1503
1628
  currentStmtOffset = Number.MAX_SAFE_INTEGER;
1504
1629
  try {
1505
1630
  recordTopLevelAssignments(f);
1506
- syntax.Walk(f, (node) => {
1631
+ walkWithCalls(f, (node) => {
1507
1632
  if (!node || result?.verdict === "block") return false;
1508
1633
  const n = node;
1509
1634
  const nodeType = syntax.NodeType(n);
@@ -2292,6 +2417,19 @@ function runnablePrefix(command) {
2292
2417
  }
2293
2418
  return null;
2294
2419
  }
2420
+ function uploadedFile(tool, flag, value) {
2421
+ const v = value.value;
2422
+ const file2 = (f) => f && f !== "-" && f !== "." ? { value: f } : null;
2423
+ if (tool === "curl") {
2424
+ if (["-d", "--data", "--data-ascii", "--data-binary", "--json"].includes(flag))
2425
+ return v.startsWith("@") ? file2(v.slice(1)) : null;
2426
+ if (flag === "--data-urlencode") return file2(/^[^=@]*@(.+)$/.exec(v)?.[1]);
2427
+ if (flag === "-F" || flag === "--form") return file2(/^[^=]*=[@<]([^;]+)/.exec(v)?.[1]);
2428
+ if (flag === "-T" || flag === "--upload-file") return file2(v);
2429
+ }
2430
+ if (tool === "wget" && ["--post-file", "--body-file"].includes(flag)) return file2(v);
2431
+ return null;
2432
+ }
2295
2433
  function merge(a, b) {
2296
2434
  a.cwd = unique([...a.cwd, ...b.cwd]);
2297
2435
  for (const key of /* @__PURE__ */ new Set([...a.vars.keys(), ...b.vars.keys()])) {
@@ -2365,6 +2503,9 @@ function wordValues(word, state) {
2365
2503
  function collectShellFileAccesses(command, context = {}) {
2366
2504
  const out = [];
2367
2505
  let payloads = 256;
2506
+ const functions = /* @__PURE__ */ new Map();
2507
+ const seenCalls = /* @__PURE__ */ new Set();
2508
+ let callWalks2 = MAX_CALL_WALKS2;
2368
2509
  const initial = {
2369
2510
  cwd: [context.cwd ?? ""],
2370
2511
  vars: /* @__PURE__ */ new Map([["HOME", [context.homeDirs?.[0] ?? "~"]]])
@@ -2622,8 +2763,98 @@ function collectShellFileAccesses(command, context = {}) {
2622
2763
  }
2623
2764
  return;
2624
2765
  }
2766
+ if (name === "wget" || name === "curl" || name === "aria2c") {
2767
+ downloadOutputs(name, args, state);
2768
+ return;
2769
+ }
2770
+ if (["http", "https", "xh", "xhs"].includes(name)) {
2771
+ for (const token of args) {
2772
+ if (!token || token.dynamic || /^[a-z][a-z0-9+.-]*:\/\//i.test(token.value)) continue;
2773
+ const m = /^(?:@|[^=:@\s]+@|[^=:@\s]*=@)(.+)$/.exec(token.value);
2774
+ if (m && m[1] !== "-") add("read", { value: m[1] }, state, { upload: true });
2775
+ }
2776
+ return;
2777
+ }
2778
+ if (name === "scp" || name === "rsync") {
2779
+ const operands2 = strings.filter((s) => s && !s.startsWith("-"));
2780
+ const remote = operands2.slice(1).some((o) => /^[^/\s]+:/.test(o));
2781
+ for (const p of copySourcePaths(words))
2782
+ add("read", locate(p), state, remote ? { upload: true } : {});
2783
+ return;
2784
+ }
2625
2785
  for (const p of copySourcePaths(words)) add("read", locate(p), state);
2626
2786
  }
2787
+ function downloadOutputs(name, args, state) {
2788
+ const valueShort = DOWNLOAD_SHORT_VALUE[name];
2789
+ const outputs = [];
2790
+ const urls = [];
2791
+ let dir2 = null;
2792
+ let remoteName = false;
2793
+ let document = false;
2794
+ let serverNamed = false;
2795
+ const take = (flag, value) => {
2796
+ if (!value) return;
2797
+ const sent = uploadedFile(name, flag, value);
2798
+ if (sent) add("read", sent, state, { upload: true });
2799
+ if (DOWNLOAD_OUTPUT_FLAGS[name].includes(flag)) {
2800
+ if (["-O", "--output-document", "--out"].includes(flag) || name === "aria2c" && flag === "-o")
2801
+ document = true;
2802
+ if (value.value !== "-" || value.pattern || value.dynamic) outputs.push(value);
2803
+ } else if (DOWNLOAD_DIR_FLAGS[name].includes(flag)) dir2 = value;
2804
+ else if (name === "wget" && (flag === "-i" || flag === "--input-file")) serverNamed = true;
2805
+ };
2806
+ for (let i = 0; i < args.length; i++) {
2807
+ const token = args[i];
2808
+ if (!token) continue;
2809
+ const v = token.value;
2810
+ if (v.startsWith("--")) {
2811
+ const eq = v.indexOf("=");
2812
+ const flag = eq < 0 ? v : v.slice(0, eq);
2813
+ if (["--remote-name", "--remote-name-all"].includes(flag)) remoteName = true;
2814
+ if (["--remote-header-name", "--recursive", "--mirror", "--page-requisites"].includes(flag))
2815
+ serverNamed = true;
2816
+ if (DOWNLOAD_LONG_VALUE[name].includes(flag))
2817
+ take(flag, eq < 0 ? args[++i] ?? null : { ...token, value: v.slice(eq + 1) });
2818
+ continue;
2819
+ }
2820
+ if (v.startsWith("-") && v.length > 1 && !token.dynamic) {
2821
+ for (let j = 1; j < v.length; j++) {
2822
+ const c = v[j];
2823
+ if (name === "wget" && c === "n") {
2824
+ j++;
2825
+ continue;
2826
+ }
2827
+ if (name === "curl" && c === "O") remoteName = true;
2828
+ if (name === "curl" && c === "J") serverNamed = true;
2829
+ if (name === "wget" && (c === "r" || c === "m" || c === "p")) serverNamed = true;
2830
+ if (valueShort.includes(c)) {
2831
+ const rest = v.slice(j + 1);
2832
+ take(`-${c}`, rest ? { value: rest } : args[++i] ?? null);
2833
+ break;
2834
+ }
2835
+ }
2836
+ continue;
2837
+ }
2838
+ if (/^(?:https?|ftps?):\/\//i.test(v)) urls.push(v);
2839
+ }
2840
+ for (const out2 of outputs) {
2841
+ const spelled = name === "aria2c" && dir2?.value && !out2.value.startsWith("/") ? { ...out2, value: `${dir2.value.replace(/\/+$/, "")}/${out2.value}` } : out2;
2842
+ if (name === "curl" && /#\d/.test(spelled.value))
2843
+ add("write", { value: "", pattern: spelled.value.replace(/#\d+/g, "*"), dot: true }, state);
2844
+ else add("write", spelled, state);
2845
+ }
2846
+ const remote = name === "curl" ? remoteName || serverNamed : !document || serverNamed;
2847
+ if (!remote) return;
2848
+ const into = dir2?.value ? dir2.value.replace(/\/+$/, "") : ".";
2849
+ if (serverNamed || urls.length === 0) {
2850
+ add("write", { value: "", pattern: `${quoteGlob(into)}/*`, dot: true }, state);
2851
+ return;
2852
+ }
2853
+ for (const url of urls) {
2854
+ const file2 = url.replace(/[?#].*$/, "").replace(/^[a-z]+:\/\/[^/]*/i, "").split("/").pop();
2855
+ add("write", { value: `${into}/${file2 || "index.html"}` }, state);
2856
+ }
2857
+ }
2627
2858
  function findActions2(tokens, words, head, state, nested, depth) {
2628
2859
  const starts = findStartPoints(words, head).starts;
2629
2860
  const roots = (starts.length ? starts : ["."]).slice(0, MAX_ALTERNATIVES);
@@ -2695,7 +2926,35 @@ function collectShellFileAccesses(command, context = {}) {
2695
2926
  const words = wordValues(node, state);
2696
2927
  alternatives = alternatives.flatMap((prefix) => (words.length ? words : [null]).map((w) => [...prefix, w])).slice(0, MAX_ALTERNATIVES);
2697
2928
  }
2698
- for (const tokens of alternatives) fromWords(tokens, state, nested, depth);
2929
+ for (const tokens of alternatives) {
2930
+ fromWords(tokens, state, nested, depth);
2931
+ inlineCall(tokens, state, depth);
2932
+ }
2933
+ }
2934
+ function inlineCall(tokens, state, depth) {
2935
+ const head = tokens[0];
2936
+ if (!head || head.dynamic || head.pattern) return;
2937
+ const bodies = functions.get(head.value);
2938
+ if (!bodies || (state.callDepth ?? 0) >= MAX_CALL_DEPTH2) return;
2939
+ const args = tokens.slice(1).map((a) => a && !a.dynamic ? a.value : null);
2940
+ const vars = JSON.stringify([...state.vars].sort(([a], [b]) => a < b ? -1 : 1));
2941
+ const key = `${head.value}\0${JSON.stringify(args)}\0${state.cwd.join("")}\0${vars}`;
2942
+ if (seenCalls.has(key)) return;
2943
+ seenCalls.add(key);
2944
+ for (const body of bodies) {
2945
+ if (callWalks2-- <= 0) return;
2946
+ const local = clone(state);
2947
+ local.callDepth = (state.callDepth ?? 0) + 1;
2948
+ for (const name of POSITIONAL) local.vars.delete(name);
2949
+ if (!rebindsPositionals(body)) {
2950
+ args.slice(0, 9).forEach((v, i) => local.vars.set(String(i + 1), v === null ? null : [v]));
2951
+ const all = args.every((v) => v !== null) && args.length ? unique(args) : null;
2952
+ local.vars.set("@", all);
2953
+ local.vars.set("*", all);
2954
+ local.vars.set("#", [String(args.length)]);
2955
+ }
2956
+ statement(body, local, depth + 1);
2957
+ }
2699
2958
  }
2700
2959
  function readsScriptFromStdin(stmt) {
2701
2960
  const cmd = stmt?.Cmd;
@@ -2732,11 +2991,13 @@ function collectShellFileAccesses(command, context = {}) {
2732
2991
  if (!stmt || depth > MAX_DEPTH) return;
2733
2992
  state.onSuccess = void 0;
2734
2993
  if (stmt.Background) state = clone(state);
2994
+ const headWord = stmt.Cmd && syntax2.NodeType(stmt.Cmd) === "CallExpr" ? base(resolveWordLiteral(stmt.Cmd.Args?.[0])) : "";
2995
+ const sends = SENDING_STDIN.has(headWord);
2735
2996
  for (const redir of stmt.Redirs ?? []) {
2736
2997
  substitutions(redir.Word, state, depth);
2737
2998
  for (const word of wordValues(redir.Word, state)) {
2738
2999
  if (WRITES.has(redir.Op)) add("write", word, state);
2739
- if (READS.has(redir.Op)) add("read", word, state);
3000
+ if (READS.has(redir.Op)) add("read", word, state, sends ? { upload: true } : {});
2740
3001
  if (redir.Op === DUP_OUT && word.value && !/^\d+$/.test(word.value))
2741
3002
  add("write", word, state);
2742
3003
  }
@@ -2845,7 +3106,10 @@ function collectShellFileAccesses(command, context = {}) {
2845
3106
  }
2846
3107
  const parsed = parseShared(command);
2847
3108
  const program2 = typeof parsed !== "symbol" ? parsed : runnablePrefix(command);
2848
- if (program2) statements(program2.Stmts ?? [], initial, 0);
3109
+ if (program2) {
3110
+ for (const [name, bodies] of functionBodies(program2)) functions.set(name, bodies);
3111
+ statements(program2.Stmts ?? [], initial, 0);
3112
+ }
2849
3113
  return out;
2850
3114
  }
2851
3115
  function isAgentInstructionPath(absPath, root) {
@@ -2961,6 +3225,33 @@ function hits(access, paths, directories, context) {
2961
3225
  }
2962
3226
  return false;
2963
3227
  }
3228
+ function hitsSystemDir(access, context, dirs = SYSTEM_DIRS) {
3229
+ const home = context.homeDirs?.[0] ?? "~";
3230
+ const candidates2 = access.candidates ?? [
3231
+ normalizeFilePath(access.path, access.cwd ?? context.cwd, home)
3232
+ ];
3233
+ for (const p of candidates2) {
3234
+ if (dirs.some((d) => within(p, d)) && !exempt(p)) return true;
3235
+ if (access.recursive && dirs.some((d) => within(d, p))) return true;
3236
+ }
3237
+ if (!access.glob) return false;
3238
+ const pattern = normalizeFilePath(access.glob, access.cwd ?? context.cwd, home);
3239
+ const magic = pattern.search(/[*?[{]/);
3240
+ const literal = magic < 0 ? pattern : pattern.slice(0, pattern.lastIndexOf("/", magic) + 1);
3241
+ if (literal && exempt(literal.replace(/\/$/, "") || "/")) return false;
3242
+ const options = { dot: access.dotGlob === true, nonegate: true };
3243
+ const components = pattern.split("/");
3244
+ for (const root of dirs) {
3245
+ const count = root.split("/").length;
3246
+ if (components.length > count && (0, import_picomatch.default)(components.slice(0, count).join("/"), options)(root))
3247
+ return true;
3248
+ if (access.recursive && (0, import_picomatch.default)(pattern, options)(root)) return true;
3249
+ }
3250
+ return false;
3251
+ }
3252
+ function uploadDirs(context) {
3253
+ return context.homeDirs?.[0] === "/root" ? UPLOAD_SYSTEM_DIRS.filter((d) => d !== "/root") : UPLOAD_SYSTEM_DIRS;
3254
+ }
2964
3255
  function evaluateProtectedFileAccess(accesses, context, settings) {
2965
3256
  const homes = [.../* @__PURE__ */ new Set([...context.homeDirs ?? ["~"], "/root"])];
2966
3257
  const startup = homes.flatMap(
@@ -2982,6 +3273,14 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
2982
3273
  "files.shell-startup",
2983
3274
  "review-shell-startup-mutation",
2984
3275
  "Changing shell startup files requires approval"
3276
+ ] : mutation && hitsSystemDir(access, context) ? [
3277
+ "files.system-dirs",
3278
+ "review-system-dir-mutation",
3279
+ "Changing files in a system directory requires approval"
3280
+ ] : !mutation && access.upload && hitsSystemDir(access, context, uploadDirs(context)) ? [
3281
+ "files.system-dirs",
3282
+ "review-system-file-upload",
3283
+ "Uploading a system file requires approval"
2985
3284
  ] : !mutation && hits(access, PASSWORD_HASH_FILES, [], context) ? [
2986
3285
  "files.password-hashes",
2987
3286
  "review-password-hash-read",
@@ -3309,16 +3608,21 @@ function looksGenerated(value, minLength, minEntropy, lettersOnlyAllowed = false
3309
3608
  if (EMAIL_VALUE_RE.test(value)) return false;
3310
3609
  if (looksLikeJoinedWords(value) || hasSequentialRun(value)) return false;
3311
3610
  if (PATH_VALUE_RE.test(value) || DOTTED_REFERENCE_RE.test(value)) return false;
3611
+ if (CONCATENATION_RE.test(value)) return false;
3312
3612
  if (rejectedByStructuredValidator(value)) return false;
3313
3613
  return shannonEntropy(value) >= minEntropy;
3314
3614
  }
3315
3615
  function nameHoldsCredential(name) {
3316
3616
  return !FIXTURE_NAME_RE.test(name) && !DERIVATIVE_NAME_RE.test(name);
3317
3617
  }
3618
+ function isOtpDocSecret(value) {
3619
+ return OTP_DOC_SECRETS.has(value.replace(/=+$/, "").toUpperCase());
3620
+ }
3318
3621
  function validateQueryCredential(raw) {
3319
3622
  const value = capturedValue(raw, QUERY_CREDENTIAL_VALUE_RE);
3320
3623
  if (value === null) return false;
3321
- if (OTP_SECRET_RE.test(value)) return looksGenerated(value, 16, 3, true);
3624
+ if (OTP_SECRET_RE.test(value))
3625
+ return !isOtpDocSecret(value) && looksGenerated(value, 16, 3, true);
3322
3626
  if (!/[0-9]/.test(value) || !/[A-Za-z]/.test(value)) return false;
3323
3627
  return looksGenerated(value, 12, 3);
3324
3628
  }
@@ -3332,13 +3636,40 @@ function keywordEndsAPart(name) {
3332
3636
  }
3333
3637
  function validateAssignedSecret(raw, before = "") {
3334
3638
  const value = capturedValue(raw, ASSIGNED_SECRET_VALUE_RE);
3335
- const name = capturedValue(raw, ASSIGNED_SECRET_NAME_RE);
3639
+ const name = capturedValue(raw, ASSIGNED_SECRET_NAME_RE)?.replace(/^_+/, "") ?? null;
3336
3640
  if (value === null || name === null) return false;
3337
- if (!keywordEndsAPart(name) || ARN_CONTEXT_RE.test(before)) return false;
3338
- const prefix = before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "";
3641
+ if (!keywordEndsAPart(name)) return false;
3642
+ if (ARN_CONTEXT_RE.test(before)) return false;
3643
+ const prefix = (before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "") + (raw.startsWith("_") ? "_" : "");
3339
3644
  if (!nameHoldsCredential(prefix + name)) return false;
3340
3645
  return looksGenerated(value, 12, 3.5);
3341
3646
  }
3647
+ function validateBasicCredential(raw) {
3648
+ const b64 = capturedValue(raw, BASIC_VALUE_RE);
3649
+ if (b64 === null) return false;
3650
+ const bare = b64.replace(/=+$/, "");
3651
+ if (bare.length % 4 === 1) return false;
3652
+ const bytes = Buffer.from(b64, "base64");
3653
+ if (bytes.toString("base64").replace(/=+$/, "") !== bare) return false;
3654
+ const text = bytes.toString("utf8");
3655
+ if (!Buffer.from(text, "utf8").equals(bytes)) return false;
3656
+ for (let i = 0; i < text.length; i++) {
3657
+ const c = text.charCodeAt(i);
3658
+ if (c < 32 || c === 127) return false;
3659
+ }
3660
+ const pair = BASIC_PAIR_RE.exec(text);
3661
+ if (!pair) return false;
3662
+ const password = pair[2];
3663
+ if (!/[0-9]/.test(password) || !/[A-Za-z]/.test(password)) return false;
3664
+ const lower2 = password.toLowerCase();
3665
+ if (DLP_STOPWORDS.some((sw) => lower2.includes(sw)) || hasSequentialRun(password)) return false;
3666
+ return shannonEntropy(password) >= 3;
3667
+ }
3668
+ function validateOtpSecret(raw) {
3669
+ const secret = capturedValue(raw, OTP_CAPTURE_RE);
3670
+ if (secret === null || isOtpDocSecret(secret)) return false;
3671
+ return looksGenerated(secret.replace(/=+$/, ""), 16, 3, true);
3672
+ }
3342
3673
  function matchSensitivePath2(resolvedPath, originalPath, options = {}) {
3343
3674
  const normalised = resolvedPath.replace(/\\/g, "/");
3344
3675
  if (!options.systemAccessHandled && isLegacySystemPath(normalised))
@@ -3647,6 +3978,156 @@ function parsePySpec(raw) {
3647
3978
  const exact = PY_EXACT_RE.exec(m[2].trim());
3648
3979
  return { name, version: exact ? exact[1] : void 0 };
3649
3980
  }
3981
+ function exactVersion(v) {
3982
+ if (!v) return void 0;
3983
+ const m = EXACT_VERSION_RE.exec(v.trim());
3984
+ return m ? m[1] : void 0;
3985
+ }
3986
+ function push(out, ecosystem, manager, name, version2, raw) {
3987
+ out.push({ ecosystem, manager, name, ...version2 ? { version: version2 } : {}, raw });
3988
+ }
3989
+ function gemInstall(words, from, out) {
3990
+ const dash = words.indexOf("--", from);
3991
+ const own = dash < 0 ? words : words.slice(0, dash);
3992
+ const pinned = exactVersion(flagValues(own, from, ["-v", "--version"])[0]);
3993
+ for (const raw of operands(own, from, GEM_VALUE_FLAGS)) {
3994
+ if (NON_REGISTRY_RE.test(raw) || /\.gem$/i.test(raw)) continue;
3995
+ const colon = raw.indexOf(":");
3996
+ const name = colon < 0 ? raw : raw.slice(0, colon);
3997
+ if (!GEM_NAME_RE.test(name)) continue;
3998
+ push(
3999
+ out,
4000
+ "RubyGems",
4001
+ "gem",
4002
+ name,
4003
+ colon < 0 ? pinned : exactVersion(raw.slice(colon + 1)),
4004
+ raw
4005
+ );
4006
+ }
4007
+ }
4008
+ function cargoCrates(words, from, manager, out) {
4009
+ const flags = manager === "cargo add" ? CARGO_ADD_VALUE_FLAGS : CARGO_INSTALL_VALUE_FLAGS;
4010
+ if (flagValues(words, from, ["--git", "--path", "--registry", "--index"]).length > 0) return;
4011
+ if (words.some((w) => w === "--git" || w === "--path")) return;
4012
+ const flagVersion = manager === "cargo install" ? exactVersion(flagValues(words, from, ["--version", "--vers"])[0]) : void 0;
4013
+ for (const raw of operands(words, from, flags)) {
4014
+ const at = raw.indexOf("@");
4015
+ const name = at < 0 ? raw : raw.slice(0, at);
4016
+ if (!CRATE_NAME_RE.test(name)) continue;
4017
+ push(
4018
+ out,
4019
+ "crates.io",
4020
+ manager,
4021
+ name,
4022
+ at < 0 ? flagVersion : exactVersion(raw.slice(at + 1)),
4023
+ raw
4024
+ );
4025
+ }
4026
+ }
4027
+ function goModules(words, from, manager, out) {
4028
+ let specs = operands(words, from, GO_VALUE_FLAGS);
4029
+ if (manager === "go run") specs = specs.slice(0, 1).filter((s) => s.includes("@"));
4030
+ for (const raw of specs) {
4031
+ const at = raw.indexOf("@");
4032
+ const pathPart = (at < 0 ? raw : raw.slice(0, at)).replace(/\/\.\.\.$/, "");
4033
+ const ver = at < 0 ? void 0 : raw.slice(at + 1);
4034
+ if (ver === "none") continue;
4035
+ if (!GO_PATH_RE.test(pathPart) || /\.go$/i.test(pathPart)) continue;
4036
+ const exact = ver && /^v\d/.test(ver) ? exactVersion(ver) : void 0;
4037
+ push(out, "Go", manager, pathPart, exact, raw);
4038
+ }
4039
+ }
4040
+ function dotnetAdd(words, from, out) {
4041
+ let k = -1;
4042
+ for (let i = from; i < Math.min(words.length, from + 2); i++) {
4043
+ if (lower(words[i]) === "package") {
4044
+ k = i;
4045
+ break;
4046
+ }
4047
+ }
4048
+ if (k < 0) return;
4049
+ const name = operands(words, k + 1, DOTNET_ADD_VALUE_FLAGS)[0];
4050
+ if (!name || !NUGET_NAME_RE.test(name)) return;
4051
+ const version2 = exactVersion(flagValues(words, k + 1, ["-v", "--version"])[0]);
4052
+ push(out, "NuGet", "dotnet add package", name, version2, name);
4053
+ }
4054
+ function dotnetTool(words, from, out) {
4055
+ const name = operands(words, from, DOTNET_TOOL_VALUE_FLAGS)[0];
4056
+ if (!name || !NUGET_NAME_RE.test(name)) return;
4057
+ const version2 = exactVersion(flagValues(words, from, ["--version"])[0]);
4058
+ push(out, "NuGet", "dotnet tool", name, version2, name);
4059
+ }
4060
+ function nugetInstall(words, from, out) {
4061
+ const lowered = words.map((w) => w !== null && w.startsWith("-") ? w.toLowerCase() : w);
4062
+ const name = operands(lowered, from, NUGET_VALUE_FLAGS)[0];
4063
+ if (!name || !NUGET_NAME_RE.test(name) || /\.config$/i.test(name)) return;
4064
+ const version2 = exactVersion(flagValues(lowered, from, ["-version"])[0]);
4065
+ push(out, "NuGet", "nuget install", name, version2, name);
4066
+ }
4067
+ function mavenGet(words, from, out) {
4068
+ const goal = words.slice(from).some(
4069
+ (w) => w !== null && /(?:^|:)dependency:get$|^org\.apache\.maven\.plugins:maven-dependency-plugin:(?:[\w.-]+:)?get$/.test(
4070
+ w
4071
+ )
4072
+ );
4073
+ if (!goal) return;
4074
+ const prop = (key) => {
4075
+ for (let i = from; i < words.length; i++) {
4076
+ const w = words[i];
4077
+ if (w === null) continue;
4078
+ if (w.startsWith(`-D${key}=`)) return w.slice(key.length + 3);
4079
+ if (w === "-D" && words[i + 1]?.startsWith(`${key}=`))
4080
+ return words[i + 1].slice(key.length + 1);
4081
+ }
4082
+ return void 0;
4083
+ };
4084
+ const artifact = prop("artifact");
4085
+ let coord;
4086
+ let ver;
4087
+ if (artifact) {
4088
+ const parts = artifact.split(":");
4089
+ if (parts.length < 2) return;
4090
+ coord = `${parts[0]}:${parts[1]}`;
4091
+ ver = parts[2];
4092
+ } else {
4093
+ const g = prop("groupId");
4094
+ const a = prop("artifactId");
4095
+ if (!g || !a) return;
4096
+ coord = `${g}:${a}`;
4097
+ ver = prop("version");
4098
+ }
4099
+ if (!MAVEN_COORD_RE.test(coord)) return;
4100
+ const exact = ver && !/^(?:LATEST|RELEASE)$/i.test(ver) ? exactVersion(ver) : void 0;
4101
+ push(out, "Maven", "mvn dependency:get", coord, exact, artifact ?? coord);
4102
+ }
4103
+ function composerPackages(words, from, manager, out) {
4104
+ const ops = operands(words, from, COMPOSER_VALUE_FLAGS);
4105
+ const specs = manager === "composer create-project" ? ops.slice(0, 1) : ops;
4106
+ for (const raw of specs) {
4107
+ const sep = raw.search(/[:=]/);
4108
+ const name = sep < 0 ? raw : raw.slice(0, sep);
4109
+ if (!COMPOSER_NAME_RE.test(name)) continue;
4110
+ let ver = sep < 0 ? void 0 : raw.slice(sep + 1);
4111
+ if (manager === "composer create-project" && ver === void 0) ver = ops[2] ?? void 0;
4112
+ push(out, "Packagist", manager, name.toLowerCase(), exactVersion(ver), raw);
4113
+ }
4114
+ }
4115
+ function editorExtensions(words, from, out) {
4116
+ for (const raw of flagValues(words, from, ["--install-extension"])) {
4117
+ if (/\.vsix$/i.test(raw) || NON_REGISTRY_RE.test(raw)) continue;
4118
+ const at = raw.lastIndexOf("@");
4119
+ const name = at <= 0 ? raw : raw.slice(0, at);
4120
+ if (!EXTENSION_ID_RE.test(name)) continue;
4121
+ push(
4122
+ out,
4123
+ "VSCode",
4124
+ "code --install-extension",
4125
+ name,
4126
+ at <= 0 ? void 0 : exactVersion(raw.slice(at + 1)),
4127
+ raw
4128
+ );
4129
+ }
4130
+ }
3650
4131
  function basename(w) {
3651
4132
  return (w ?? "").toLowerCase().split("/").pop() ?? "";
3652
4133
  }
@@ -3878,7 +4359,63 @@ function fromCall(words, out, depth) {
3878
4359
  pushPy(out, "pipenv", operands(words, v + 1, PIPENV_VALUE_FLAGS));
3879
4360
  return;
3880
4361
  }
4362
+ case "gem": {
4363
+ const v = verbIndex(words, start + 1, GEM_VALUE_FLAGS);
4364
+ if (v >= 0 && ["install", "i"].includes(lower(words[v]))) gemInstall(words, v + 1, out);
4365
+ return;
4366
+ }
4367
+ case "bundle": {
4368
+ const v = verbIndex(words, start + 1, BUNDLE_VALUE_FLAGS);
4369
+ if (v < 0 || lower(words[v]) !== "add") return;
4370
+ if (flagValues(words, v + 1, ["--git", "--github", "--path"]).length > 0) return;
4371
+ const version2 = exactVersion(flagValues(words, v + 1, ["-v", "--version"])[0]);
4372
+ for (const raw of operands(words, v + 1, BUNDLE_VALUE_FLAGS))
4373
+ if (GEM_NAME_RE.test(raw)) push(out, "RubyGems", "bundle add", raw, version2, raw);
4374
+ return;
4375
+ }
4376
+ case "cargo": {
4377
+ const from = words[start + 1]?.startsWith("+") ? start + 2 : start + 1;
4378
+ const v = verbIndex(words, from, CARGO_ADD_VALUE_FLAGS);
4379
+ const verb = lower(words[v]);
4380
+ if (verb === "add") cargoCrates(words, v + 1, "cargo add", out);
4381
+ else if (verb === "install") cargoCrates(words, v + 1, "cargo install", out);
4382
+ return;
4383
+ }
4384
+ case "go": {
4385
+ const v = verbIndex(words, start + 1, GO_VALUE_FLAGS);
4386
+ const verb = lower(words[v]);
4387
+ if (verb === "get" || verb === "install" || verb === "run")
4388
+ goModules(words, v + 1, `go ${verb}`, out);
4389
+ return;
4390
+ }
4391
+ case "dotnet": {
4392
+ const v = verbIndex(words, start + 1, DOTNET_ADD_VALUE_FLAGS);
4393
+ const verb = lower(words[v]);
4394
+ if (verb === "add") dotnetAdd(words, v + 1, out);
4395
+ else if (verb === "tool" && ["install", "update"].includes(lower(words[v + 1])))
4396
+ dotnetTool(words, v + 2, out);
4397
+ return;
4398
+ }
4399
+ case "nuget": {
4400
+ if (lower(words[start + 1]) === "install") nugetInstall(words, start + 2, out);
4401
+ return;
4402
+ }
4403
+ case "mvn":
4404
+ case "mvnw":
4405
+ mavenGet(words, start + 1, out);
4406
+ return;
4407
+ case "composer": {
4408
+ let v = verbIndex(words, start + 1, COMPOSER_VALUE_FLAGS);
4409
+ if (v >= 0 && lower(words[v]) === "global") v = verbIndex(words, v + 1, COMPOSER_VALUE_FLAGS);
4410
+ const verb = lower(words[v]);
4411
+ if (verb === "require" || verb === "req" || verb === "r")
4412
+ composerPackages(words, v + 1, "composer require", out);
4413
+ else if (verb === "create-project")
4414
+ composerPackages(words, v + 1, "composer create-project", out);
4415
+ return;
4416
+ }
3881
4417
  default:
4418
+ if (EDITOR_HEADS.includes(family)) editorExtensions(words, start + 1, out);
3882
4419
  return;
3883
4420
  }
3884
4421
  }
@@ -5404,12 +5941,13 @@ function gitRepoPath(url) {
5404
5941
  function isDestructiveToolName(bareTool) {
5405
5942
  return DESTRUCTIVE_VERB_RE.test(bareTool);
5406
5943
  }
5407
- function* validCardRuns(text, base2) {
5944
+ function* validCardRuns(text, base2, exemptFractions = true) {
5408
5945
  const re = new RegExp(base2.source, "g");
5409
5946
  let m;
5410
5947
  while ((m = re.exec(text)) !== null) {
5411
5948
  const digits = m[0].replace(/\D/g, "");
5412
- if (!isDecimalFraction(text, m.index, digits.length === m[0].length) && validateLuhn(digits)) {
5949
+ const fraction = exemptFractions && isDecimalFraction(text, m.index, digits.length === m[0].length);
5950
+ if (!fraction && validateLuhn(digits)) {
5413
5951
  yield { index: m.index, length: m[0].length, digits };
5414
5952
  } else {
5415
5953
  re.lastIndex = m.index + 1;
@@ -5422,12 +5960,12 @@ function hasValidCard(text) {
5422
5960
  }
5423
5961
  return false;
5424
5962
  }
5425
- function replaceValidCards(text, replace) {
5963
+ function replaceValidCards(text, replace, { exemptFractions = true } = {}) {
5426
5964
  let out = text;
5427
5965
  for (const base2 of [PII_CC16_RE, PII_CC15_RE]) {
5428
5966
  let res = "";
5429
5967
  let last = 0;
5430
- for (const run of validCardRuns(out, base2)) {
5968
+ for (const run of validCardRuns(out, base2, exemptFractions)) {
5431
5969
  res += out.slice(last, run.index) + replace(run.digits);
5432
5970
  last = run.index + run.length;
5433
5971
  }
@@ -5441,11 +5979,11 @@ function isDecimalFraction(text, index, unseparated) {
5441
5979
  const a = text[index - 2];
5442
5980
  if (sep === ".") {
5443
5981
  if (a !== void 0 && a >= "0" && a <= "9") return true;
5444
- return a === void 0 || NUMBER_LEAD_RE.test(a);
5982
+ return a === void 0 || (a === "-" || a === "+") && index === 2;
5445
5983
  }
5446
5984
  if (sep === "," && a === "0") {
5447
5985
  const b = text[index - 3];
5448
- return b === void 0 || NUMBER_LEAD_RE.test(b);
5986
+ return b === void 0 || b === ";";
5449
5987
  }
5450
5988
  return false;
5451
5989
  }
@@ -5581,6 +6119,9 @@ function normalizeTrustedEntry(raw) {
5581
6119
  return full === null ? null : trustedEntryOf(full);
5582
6120
  }
5583
6121
  function maskPersonalData(text) {
6122
+ return maskText(text, false);
6123
+ }
6124
+ function maskText(text, exemptFractions) {
5584
6125
  if (!text) return text;
5585
6126
  try {
5586
6127
  let out = text;
@@ -5588,7 +6129,9 @@ function maskPersonalData(text) {
5588
6129
  const v = m.replace(/[ -]/g, "").toUpperCase();
5589
6130
  return validateIban(v) ? `${v.slice(0, 4)} **** **** ${v.slice(-4)}` : m;
5590
6131
  });
5591
- out = replaceValidCards(out, (digits) => `**** **** **** ${digits.slice(-4)}`);
6132
+ out = replaceValidCards(out, (digits) => `**** **** **** ${digits.slice(-4)}`, {
6133
+ exemptFractions
6134
+ });
5592
6135
  out = out.replace(
5593
6136
  ACCOUNT_VALUE_RE,
5594
6137
  (m, v) => IBAN_LENGTH[v.slice(0, 2).toUpperCase()] === v.length ? `"****${v.slice(-4)}"` : m
@@ -5611,7 +6154,7 @@ function maskLeaf(value, key, depth) {
5611
6154
  if (depth > MAX_MASK_DEPTH) return "[REDACTED]";
5612
6155
  if (typeof value === "string") {
5613
6156
  try {
5614
- const json = `{${JSON.stringify(key)}:${JSON.stringify(value)}}`;
6157
+ const json = `{${JSON.stringify(key)}:${JSON.stringify(maskPersonalData(value))}}`;
5615
6158
  return Object.values(JSON.parse(maskPersonalData(json)))[0];
5616
6159
  } catch {
5617
6160
  return "[REDACTED]";
@@ -5619,13 +6162,20 @@ function maskLeaf(value, key, depth) {
5619
6162
  }
5620
6163
  if (typeof value === "number") {
5621
6164
  const text = String(value);
5622
- const masked = maskPersonalData(text);
6165
+ const masked = maskText(text, true);
5623
6166
  return masked === text ? value : masked;
5624
6167
  }
5625
6168
  if (Array.isArray(value)) return value.map((v) => maskLeaf(v, key, depth + 1));
5626
6169
  if (value && typeof value === "object") {
6170
+ const used = /* @__PURE__ */ new Set();
5627
6171
  return Object.fromEntries(
5628
- Object.entries(value).map(([k, v]) => [maskPersonalData(k), maskLeaf(v, k, depth + 1)])
6172
+ Object.entries(value).map(([k, v]) => {
6173
+ const base2 = maskPersonalData(k);
6174
+ let name = base2;
6175
+ for (let n = 2; used.has(name); n++) name = `${base2}#${n}`;
6176
+ used.add(name);
6177
+ return [name, maskLeaf(v, k, depth + 1)];
6178
+ })
5629
6179
  );
5630
6180
  }
5631
6181
  return value;
@@ -6633,7 +7183,7 @@ function* stringValues(obj, depth = 0) {
6633
7183
  }
6634
7184
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
6635
7185
  }
6636
- var import_picomatch, import_safe_regex2, import_picomatch2, import_mvdan_sh, import_safe_regex22, import_mvdan_sh2, import_safe_regex23, import_crypto2, import_crypto3, import_mvdan_sh3, import_crypto4, import_mvdan_sh4, import_crypto5, 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;
7186
+ var import_picomatch, import_safe_regex2, import_picomatch2, import_mvdan_sh, import_safe_regex22, import_mvdan_sh2, import_safe_regex23, import_crypto2, import_crypto3, import_mvdan_sh3, import_crypto4, import_mvdan_sh4, import_crypto5, 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;
6637
7187
  var init_dist = __esm({
6638
7188
  "packages/policy-engine/dist/index.mjs"() {
6639
7189
  "use strict";
@@ -7447,6 +7997,11 @@ var init_dist = __esm({
7447
7997
  example: "cat /etc/shadow",
7448
7998
  advice: "Keep Review, or choose Block when agents must never read system password hashes."
7449
7999
  },
8000
+ "files.system-dirs": {
8001
+ 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.",
8002
+ example: "wget -O /usr/bin/tool https://example.com/tool",
8003
+ advice: "Keep Review on a workstation. A container image build that installs into /usr can set it to Log."
8004
+ },
7450
8005
  // ── Commands ──────────────────────────────────────────────────────────────
7451
8006
  "commands.inline-exec": {
7452
8007
  plain: "The agent runs a small program written inside the command itself.",
@@ -7643,7 +8198,7 @@ var init_dist = __esm({
7643
8198
  advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
7644
8199
  },
7645
8200
  "loading.malicious-package": {
7646
- plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago.",
8201
+ 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.",
7647
8202
  example: "npm install of a package flagged as malware",
7648
8203
  advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
7649
8204
  }
@@ -7689,6 +8244,15 @@ var init_dist = __esm({
7689
8244
  defaultValue: "review",
7690
8245
  legacy: ["review-password-hash-read"]
7691
8246
  },
8247
+ {
8248
+ id: "files.system-dirs",
8249
+ group: "files",
8250
+ title: "System directories",
8251
+ catches: "Writes, new files and removals under /usr, /etc, /boot, /bin, /sbin, /lib and /var/log, and uploads of system files",
8252
+ values: ALL,
8253
+ defaultValue: "review",
8254
+ legacy: ["review-system-dir-mutation", "review-system-file-upload"]
8255
+ },
7692
8256
  // ── Commands ────────────────────────────────────────────────────────────
7693
8257
  {
7694
8258
  id: "commands.inline-exec",
@@ -8986,6 +9550,10 @@ var init_dist = __esm({
8986
9550
  AND_OP = probeBinOp("a && b");
8987
9551
  OR_OP = probeBinOp("a || b");
8988
9552
  AND_OR_OPS = new Set([AND_OP, OR_OP].filter((o) => o !== null));
9553
+ MAX_CALL_DEPTH = 4;
9554
+ MAX_CALL_WALKS = 256;
9555
+ callDepth = 0;
9556
+ callWalks = 0;
8989
9557
  FS_OP_CACHE_MAX = 5e3;
8990
9558
  fsOpCache = /* @__PURE__ */ new Map();
8991
9559
  stripDotSlash = (p) => p.replace(/^\.\//, "");
@@ -9038,10 +9606,79 @@ var init_dist = __esm({
9038
9606
  MAX_ARGV_CHARACTERS = 262144;
9039
9607
  ({ syntax: syntax2 } = import_mvdan_sh2.default);
9040
9608
  MAX_ALTERNATIVES = 64;
9609
+ MAX_CALL_DEPTH2 = 4;
9610
+ MAX_CALL_WALKS2 = 256;
9611
+ POSITIONAL = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*", "#"];
9041
9612
  MAX_DEPTH = 64;
9042
9613
  MAX_PAYLOAD_DEPTH = 24;
9043
9614
  unique = (a) => [...new Set(a)].slice(0, MAX_ALTERNATIVES);
9044
- clone = (s) => ({ cwd: [...s.cwd], vars: new Map(s.vars) });
9615
+ clone = (s) => ({
9616
+ cwd: [...s.cwd],
9617
+ vars: new Map(s.vars),
9618
+ ...s.callDepth ? { callDepth: s.callDepth } : {}
9619
+ });
9620
+ SENDING_STDIN = /* @__PURE__ */ new Set(["nc", "ncat", "netcat", "socat", "telnet", "curl", "wget"]);
9621
+ DOWNLOAD_SHORT_VALUE = {
9622
+ curl: "odDeEFHKmPrtTuUwxXyYzAbcCQ",
9623
+ wget: "OoaPiBetTwQlARDXIUY",
9624
+ aria2c: "odisx"
9625
+ };
9626
+ DOWNLOAD_LONG_VALUE = {
9627
+ curl: [
9628
+ "--output",
9629
+ "--output-dir",
9630
+ "--dump-header",
9631
+ "--cookie-jar",
9632
+ "--data",
9633
+ "--data-binary",
9634
+ "--data-raw",
9635
+ "--data-ascii",
9636
+ "--data-urlencode",
9637
+ "--form",
9638
+ "--header",
9639
+ "--request",
9640
+ "--user",
9641
+ "--user-agent",
9642
+ "--referer",
9643
+ "--cookie",
9644
+ "--config",
9645
+ "--json",
9646
+ "--upload-file",
9647
+ "--max-time",
9648
+ "--proxy",
9649
+ "--write-out",
9650
+ "--url"
9651
+ ],
9652
+ wget: [
9653
+ "--output-document",
9654
+ "--output-file",
9655
+ "--append-output",
9656
+ "--directory-prefix",
9657
+ "--input-file",
9658
+ "--post-data",
9659
+ "--post-file",
9660
+ "--body-data",
9661
+ "--body-file",
9662
+ "--method",
9663
+ "--header",
9664
+ "--user-agent",
9665
+ "--user",
9666
+ "--password",
9667
+ "--tries",
9668
+ "--timeout"
9669
+ ],
9670
+ aria2c: ["--out", "--dir", "--input-file"]
9671
+ };
9672
+ DOWNLOAD_OUTPUT_FLAGS = {
9673
+ curl: ["-o", "--output", "-D", "--dump-header", "-c", "--cookie-jar"],
9674
+ wget: ["-O", "--output-document", "-o", "--output-file", "-a", "--append-output"],
9675
+ aria2c: ["-o", "--out"]
9676
+ };
9677
+ DOWNLOAD_DIR_FLAGS = {
9678
+ curl: ["--output-dir"],
9679
+ wget: ["-P", "--directory-prefix"],
9680
+ aria2c: ["-d", "--dir"]
9681
+ };
9045
9682
  base = (s) => (s ?? "").replace(/\/+$/, "").split("/").pop() ?? "";
9046
9683
  quoteGlob = (s) => s.replace(/[\\*?\[\]{}()!+@]/g, "\\$&");
9047
9684
  asWord = (p) => ({ value: p.value, ...p.glob ? { pattern: p.pattern } : {} });
@@ -9066,6 +9703,18 @@ var init_dist = __esm({
9066
9703
  SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
9067
9704
  PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
9068
9705
  STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
9706
+ SYSTEM_DIRS = [
9707
+ "/usr",
9708
+ "/etc",
9709
+ "/boot",
9710
+ "/bin",
9711
+ "/sbin",
9712
+ "/lib",
9713
+ "/lib64",
9714
+ "/var/log"
9715
+ ];
9716
+ SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
9717
+ UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
9069
9718
  AGENT_INSTRUCTION_FILES = [
9070
9719
  ".gitlab-ci.yml",
9071
9720
  "CLAUDE.md",
@@ -9083,6 +9732,7 @@ var init_dist = __esm({
9083
9732
  ".husky"
9084
9733
  ];
9085
9734
  LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
9735
+ exempt = (p) => SYSTEM_DIRS_EXEMPT.some((d) => within(p, d));
9086
9736
  RANK = { allow: 0, review: 1, block: 2 };
9087
9737
  BIP39_ENGLISH = `abandon
9088
9738
  ability
@@ -11409,15 +12059,20 @@ zoo`.split("\n");
11409
12059
  IDENTIFIER_VALUE_RE = /^[A-Za-z_.-]+$/;
11410
12060
  PATH_VALUE_RE = /^(?:\/|\.{1,2}\/|~\/)/;
11411
12061
  DOTTED_REFERENCE_RE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/;
12062
+ CONCATENATION_RE = /^\+[A-Za-z_$][\w$]*\./;
11412
12063
  EMAIL_VALUE_RE = /^[^@\s]+@[^@\s]+\.[A-Za-z]{2,}$/;
11413
12064
  FIXTURE_NAME_RE = /(?:fake|dummy|example|sample|mock|stub|placeholder|test)/i;
11414
- 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;
12065
+ 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;
11415
12066
  QUERY_CREDENTIAL_VALUE_RE = /=([A-Za-z0-9%+/_.~-]{8,512}={0,2})$/;
11416
12067
  OTP_SECRET_RE = /^[A-Z2-7]{16,}=*$/i;
11417
- ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
12068
+ OTP_DOC_SECRETS = /* @__PURE__ */ new Set(["JBSWY3DPEHPK3PXP", "GEZDGNBVGY3TQOJQ"]);
12069
+ ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
11418
12070
  ASSIGNED_SECRET_NAME_RE = /^([A-Za-z0-9_]+)/;
11419
- NAME_KEYWORD_RE = /^(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret)/i;
12071
+ 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;
11420
12072
  ARN_CONTEXT_RE = /\barn:[A-Za-z0-9-]*:[^\s"'`]*$/i;
12073
+ BASIC_VALUE_RE = /([A-Za-z0-9+/]{8,512}={0,2})$/;
12074
+ BASIC_PAIR_RE = /^([^:\s]{1,128}):(\S{8,256})$/;
12075
+ OTP_CAPTURE_RE = /["']([A-Z2-7]{16,128}=*)["']$/i;
11421
12076
  DLP_PATTERNS = [
11422
12077
  // ── AWS ───────────────────────────────────────────────────────────────────
11423
12078
  {
@@ -11755,6 +12410,22 @@ zoo`.split("\n");
11755
12410
  contextBoost: true,
11756
12411
  minEntropy: 3
11757
12412
  },
12413
+ // ── HTTP Basic credential ─────────────────────────────────────────────────
12414
+ // `Authorization: Basic <base64 of user:password>`, or the bare scheme in a
12415
+ // config list. The validator decodes the value and accepts only a
12416
+ // `user:password` whose password looks generated (letters and digits, 8+
12417
+ // characters, the query-credential rule), so prose ("Basic authentication")
12418
+ // and documentation pairs (`admin:changeme`, `user:pass1234`) stay allowed.
12419
+ // review: the same weight as a bare Bearer token.
12420
+ {
12421
+ name: "HTTP Basic Credential",
12422
+ regex: /\bBasic[ \t]{1,8}([A-Za-z0-9+/]{8,512}={0,2})/i,
12423
+ severity: "review",
12424
+ keywords: ["basic"],
12425
+ validate: validateBasicCredential,
12426
+ redactGroup: 1,
12427
+ argumentsOnly: true
12428
+ },
11758
12429
  // ── Resend ────────────────────────────────────────────────────────────────
11759
12430
  {
11760
12431
  name: "Resend API Key",
@@ -11896,6 +12567,19 @@ zoo`.split("\n");
11896
12567
  validate: validateQueryCredential,
11897
12568
  argumentsOnly: true
11898
12569
  },
12570
+ // ── OTP secret in a constructor ───────────────────────────────────────────
12571
+ // `ROTP::TOTP.new('<base32>')`, `pyotp.TOTP("<base32>")`, `new HOTP('...')`:
12572
+ // the shared secret of a one-time-password generator written as a literal.
12573
+ // The otpauth URI form is `Credential in URL Query`. review.
12574
+ {
12575
+ name: "OTP Secret",
12576
+ regex: /\b(?:TOTP|HOTP|OTP)(?:\.new|\.create)?[ \t]{0,8}\(?[ \t]{0,8}["']([A-Z2-7]{16,128}=*)["']/i,
12577
+ severity: "review",
12578
+ keywords: ["otp"],
12579
+ validate: validateOtpSecret,
12580
+ redactGroup: 1,
12581
+ argumentsOnly: true
12582
+ },
11899
12583
  // ── Secret assigned to a telling name ─────────────────────────────────────
11900
12584
  // `password: ...`, `JWT_SECRET=...`, `api_key = "..."`. The name alone is
11901
12585
  // not a finding: most such lines hold a reference (`process.env.X`), a
@@ -11903,12 +12587,14 @@ zoo`.split("\n");
11903
12587
  // generated: 12+ characters, not an identifier, Shannon entropy of 3.5
11904
12588
  // bits per character or more. review, never block (node9Firewall#348,
11905
12589
  // item 4). Tuned on CredData's public split; its look-alike lines are the
11906
- // negative control.
12590
+ // negative control. A quote may be escaped (`\"access_token\":\"...\"`):
12591
+ // JSON written inside a string literal. The two optional quotes after the
12592
+ // name are written out, not as a counted group, to keep star height 1.
11907
12593
  {
11908
12594
  name: "Assigned Secret",
11909
- 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,
12595
+ 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,
11910
12596
  severity: "review",
11911
- keywords: ["password", "passphrase", "passwd", "pwd", "secret", "token", "key"],
12597
+ keywords: ["pass", "pwd", "secret", "token", "key", "ticket"],
11912
12598
  validate: validateAssignedSecret,
11913
12599
  argumentsOnly: true
11914
12600
  }
@@ -12176,7 +12862,169 @@ zoo`.split("\n");
12176
12862
  ]);
12177
12863
  PY_SPEC_RE = /^([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)(?:\[[^\]]*\])?\s*(.*)$/;
12178
12864
  PY_EXACT_RE = /^===?\s*v?(\d+(?:\.\d+)*(?:[a-zA-Z0-9.+!-]*))$/;
12865
+ EXACT_VERSION_RE = /^=?v?(\d+(?:\.\d+)*(?:[-+][0-9A-Za-z][0-9A-Za-z.+-]*)?)$/;
12866
+ GEM_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
12867
+ GEM_VALUE_FLAGS = /* @__PURE__ */ new Set([
12868
+ "-v",
12869
+ "--version",
12870
+ "-i",
12871
+ "--install-dir",
12872
+ "-n",
12873
+ "--bindir",
12874
+ "--document",
12875
+ "--build-root",
12876
+ "-P",
12877
+ "--trust-policy",
12878
+ "-s",
12879
+ "--source",
12880
+ "-p",
12881
+ "--http-proxy",
12882
+ "--platform",
12883
+ "-g",
12884
+ "--file",
12885
+ "--without"
12886
+ ]);
12887
+ BUNDLE_VALUE_FLAGS = /* @__PURE__ */ new Set([
12888
+ "-v",
12889
+ "--version",
12890
+ "-g",
12891
+ "--group",
12892
+ "-s",
12893
+ "--source",
12894
+ "-r",
12895
+ "--require",
12896
+ "--path",
12897
+ "--git",
12898
+ "--github",
12899
+ "--branch",
12900
+ "--ref",
12901
+ "--glob"
12902
+ ]);
12903
+ CRATE_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_-]*$/u;
12904
+ CARGO_ADD_VALUE_FLAGS = /* @__PURE__ */ new Set([
12905
+ "-F",
12906
+ "--features",
12907
+ "--rename",
12908
+ "--registry",
12909
+ "--manifest-path",
12910
+ "-p",
12911
+ "--package",
12912
+ "--git",
12913
+ "--branch",
12914
+ "--tag",
12915
+ "--rev",
12916
+ "--path",
12917
+ "--base",
12918
+ "--target",
12919
+ "--color",
12920
+ "--config",
12921
+ "-Z",
12922
+ "--lockfile-path"
12923
+ ]);
12924
+ CARGO_INSTALL_VALUE_FLAGS = /* @__PURE__ */ new Set([
12925
+ "--version",
12926
+ "--vers",
12927
+ "--git",
12928
+ "--branch",
12929
+ "--tag",
12930
+ "--rev",
12931
+ "--path",
12932
+ "--root",
12933
+ "--index",
12934
+ "--registry",
12935
+ "-F",
12936
+ "--features",
12937
+ "--profile",
12938
+ "--target",
12939
+ "--target-dir",
12940
+ "-j",
12941
+ "--jobs",
12942
+ "--bin",
12943
+ "--example",
12944
+ "--color",
12945
+ "--config",
12946
+ "-Z"
12947
+ ]);
12948
+ GO_VALUE_FLAGS = /* @__PURE__ */ new Set([
12949
+ "-C",
12950
+ "-tags",
12951
+ "-ldflags",
12952
+ "-gcflags",
12953
+ "-asmflags",
12954
+ "-mod",
12955
+ "-modfile",
12956
+ "-o",
12957
+ "-p",
12958
+ "-pkgdir",
12959
+ "-toolexec",
12960
+ "-exec",
12961
+ "-overlay",
12962
+ "-pgo",
12963
+ "-buildmode",
12964
+ "-compiler",
12965
+ "-gccgoflags",
12966
+ "-installsuffix",
12967
+ "-covermode",
12968
+ "-coverpkg"
12969
+ ]);
12970
+ GO_PATH_RE = /^[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+(?:\/[\p{L}\p{N}_.~+-]+)*$/u;
12971
+ NUGET_NAME_RE = /^[\p{L}\p{N}_][\p{L}\p{N}_.-]*$/u;
12972
+ DOTNET_ADD_VALUE_FLAGS = /* @__PURE__ */ new Set([
12973
+ "-v",
12974
+ "--version",
12975
+ "-s",
12976
+ "--source",
12977
+ "-f",
12978
+ "--framework",
12979
+ "--package-directory"
12980
+ ]);
12981
+ DOTNET_TOOL_VALUE_FLAGS = /* @__PURE__ */ new Set([
12982
+ "--version",
12983
+ "--add-source",
12984
+ "--configfile",
12985
+ "--framework",
12986
+ "--tool-path",
12987
+ "-a",
12988
+ "--arch",
12989
+ "--tool-manifest",
12990
+ "-v",
12991
+ "--verbosity"
12992
+ ]);
12993
+ NUGET_VALUE_FLAGS = /* @__PURE__ */ new Set([
12994
+ "-version",
12995
+ "-outputdirectory",
12996
+ "-o",
12997
+ "-source",
12998
+ "-configfile",
12999
+ "-fallbacksource",
13000
+ "-framework",
13001
+ "-packagesavemode",
13002
+ "-dependencyversion",
13003
+ "-solutiondirectory",
13004
+ "-verbosity"
13005
+ ]);
13006
+ MAVEN_COORD_RE = /^[\p{L}\p{N}_.-]+:[\p{L}\p{N}_.-]+$/u;
13007
+ COMPOSER_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_.-]*\/[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
13008
+ COMPOSER_VALUE_FLAGS = /* @__PURE__ */ new Set([
13009
+ "-d",
13010
+ "--working-dir",
13011
+ "--stability",
13012
+ "--repository",
13013
+ "--add-repository"
13014
+ ]);
13015
+ EDITOR_HEADS = ["code", "code-insiders", "codium", "vscodium", "cursor"];
13016
+ EXTENSION_ID_RE = /^[\p{L}\p{N}][\p{L}\p{N}_-]*\.[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
12179
13017
  MANAGER_HEADS = /* @__PURE__ */ new Set([
13018
+ "gem",
13019
+ "bundle",
13020
+ "cargo",
13021
+ "go",
13022
+ "dotnet",
13023
+ "nuget",
13024
+ "mvn",
13025
+ "mvnw",
13026
+ "composer",
13027
+ ...EDITOR_HEADS,
12180
13028
  "npm",
12181
13029
  "npx",
12182
13030
  "pnpm",
@@ -12255,6 +13103,7 @@ zoo`.split("\n");
12255
13103
  MAP_GOVERNED = /* @__PURE__ */ new Set([
12256
13104
  "files.shell-startup",
12257
13105
  "files.password-hashes",
13106
+ "files.system-dirs",
12258
13107
  "commands.inline-exec",
12259
13108
  "commands.eval-dynamic",
12260
13109
  "commands.curl-pipe-shell",
@@ -12523,7 +13372,6 @@ zoo`.split("\n");
12523
13372
  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/;
12524
13373
  PII_CC15_RE = /\b3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}\b/;
12525
13374
  PII_IBAN_RE = /\b[A-Z]{2}\d{2}(?:[A-Z0-9]|[ -][A-Z0-9]){11,30}\b/;
12526
- NUMBER_LEAD_RE = /[\s:=;([{"'+-]/;
12527
13375
  REALTIME_PII_PATTERNS = ["SSN", "Credit Card", "IBAN"];
12528
13376
  MAX_PII_SCAN_BYTES = 1e5;
12529
13377
  MASKED_KINDS = /* @__PURE__ */ new Set(["email", "iban", "account"]);
@@ -12745,7 +13593,7 @@ zoo`.split("\n");
12745
13593
  { view: "separators-stripped", decoder: "separators", stripped: true }
12746
13594
  ];
12747
13595
  LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
12748
- CANONICAL_EXTRACTOR_VERSION = "canonical-v27";
13596
+ CANONICAL_EXTRACTOR_VERSION = "canonical-v28";
12749
13597
  DEDUPE_PREVIEW_LEN = 120;
12750
13598
  ENGINE_VERSION = "1.4.0";
12751
13599
  }
@@ -16153,14 +17001,23 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
16153
17001
  clearTimeout(timer);
16154
17002
  }
16155
17003
  }
16156
- async function waitForDaemonDecision(id, signal) {
17004
+ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
16157
17005
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
16158
17006
  const waitCtrl = new AbortController();
16159
- const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
17007
+ let capped = false;
17008
+ const waitTimer = maxWaitMs > 0 ? setTimeout(() => {
17009
+ capped = true;
17010
+ waitCtrl.abort();
17011
+ }, maxWaitMs) : void 0;
16160
17012
  const onAbort = () => waitCtrl.abort();
16161
17013
  if (signal) signal.addEventListener("abort", onAbort);
16162
17014
  try {
16163
- const waitRes = await fetch(`${base2}/wait/${id}`, { signal: waitCtrl.signal });
17015
+ const waitRes = await fetch(`${base2}/wait/${id}`, { signal: waitCtrl.signal }).catch(
17016
+ (err2) => {
17017
+ if (capped && !signal?.aborted) throw new Error("node9 daemon did not answer in time");
17018
+ throw err2;
17019
+ }
17020
+ );
16164
17021
  if (!waitRes.ok) return { decision: "deny" };
16165
17022
  const { decision, source, reason } = await waitRes.json();
16166
17023
  if (decision === "allow") return { decision: "allow", source };
@@ -17790,13 +18647,13 @@ function maskRiskMetadataForCloud(rm) {
17790
18647
  const r = rm;
17791
18648
  const dests = Array.isArray(r.exposureDestinations) ? r.exposureDestinations.filter((d) => typeof d === "string") : [];
17792
18649
  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);
17793
- const maskText = (text) => {
18650
+ const maskText2 = (text) => {
17794
18651
  let out2 = text;
17795
18652
  for (const [full, masked] of swaps) out2 = out2.split(full).join(masked);
17796
18653
  return maskPersonalData(out2);
17797
18654
  };
17798
18655
  const out = {};
17799
- for (const [k, v] of Object.entries(r)) out[k] = typeof v === "string" ? maskText(v) : v;
18656
+ for (const [k, v] of Object.entries(r)) out[k] = typeof v === "string" ? maskText2(v) : v;
17800
18657
  if (dests.length > 0) {
17801
18658
  out.exposureDestinations = dests.map(maskDestination);
17802
18659
  out.exposureDestinationHashes = dests.map((d) => isMaskedKind(d) ? hashDestination(d) : "");
@@ -18099,7 +18956,20 @@ function ecoDir(eco) {
18099
18956
  return import_path24.default.join(osvRoot(), eco);
18100
18957
  }
18101
18958
  function indexKey(eco, name) {
18102
- return eco === "PyPI" ? normalizePyPiName(name) : name.toLowerCase();
18959
+ switch (eco) {
18960
+ case "PyPI":
18961
+ return normalizePyPiName(name);
18962
+ case "crates.io":
18963
+ return name.toLowerCase().replace(/_/g, "-");
18964
+ case "Go":
18965
+ case "Maven":
18966
+ return name;
18967
+ default:
18968
+ return name.toLowerCase();
18969
+ }
18970
+ }
18971
+ function recordEcosystemMatches(eco, recordEco) {
18972
+ return typeof recordEco === "string" && (recordEco === eco || recordEco.startsWith(`${eco}:`));
18103
18973
  }
18104
18974
  function shardOf(key) {
18105
18975
  return (0, import_crypto11.createHash)("sha1").update(key).digest("hex").slice(0, 2);
@@ -18145,7 +19015,8 @@ function entriesFromRecord(eco, record2) {
18145
19015
  const modifiedMs = typeof r.modified === "string" ? Date.parse(r.modified) || 0 : 0;
18146
19016
  const rows = /* @__PURE__ */ new Map();
18147
19017
  for (const a of r.affected ?? []) {
18148
- if (a?.package?.ecosystem !== eco || typeof a.package.name !== "string") continue;
19018
+ if (!recordEcosystemMatches(eco, a?.package?.ecosystem) || typeof a?.package?.name !== "string")
19019
+ continue;
18149
19020
  const key = indexKey(eco, a.package.name);
18150
19021
  const entry = rows.get(key) ?? { id: r.id };
18151
19022
  if (Array.isArray(a.versions)) {
@@ -18388,9 +19259,11 @@ async function pypiInfo(name, version2) {
18388
19259
  };
18389
19260
  }
18390
19261
  async function registryInfo(eco, name, version2, freshWindowMs, raw) {
18391
- return eco === "npm" ? npmInfo(name, version2, freshWindowMs, raw) : pypiInfo(name, version2);
19262
+ if (eco === "npm") return npmInfo(name, version2, freshWindowMs, raw);
19263
+ if (eco === "PyPI") return pypiInfo(name, version2);
19264
+ return null;
18392
19265
  }
18393
- var import_semver, TIMEOUT_MS2, MAX_BYTES;
19266
+ var import_semver, TIMEOUT_MS2, MAX_BYTES, HAS_REGISTRY_CLIENT;
18394
19267
  var init_registry2 = __esm({
18395
19268
  "src/supply-chain/registry.ts"() {
18396
19269
  "use strict";
@@ -18398,6 +19271,7 @@ var init_registry2 = __esm({
18398
19271
  init_net();
18399
19272
  TIMEOUT_MS2 = 1500;
18400
19273
  MAX_BYTES = 2 * 1024 * 1024;
19274
+ HAS_REGISTRY_CLIENT = /* @__PURE__ */ new Set(["npm", "PyPI"]);
18401
19275
  }
18402
19276
  });
18403
19277
 
@@ -18442,25 +19316,39 @@ function commandOf(toolName, args) {
18442
19316
  return typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : null;
18443
19317
  }
18444
19318
  function label(p, version2) {
18445
- return `${p.ecosystem === "npm" ? "npm" : "PyPI"} package "${p.name}${version2 ? `@${version2}` : ""}"`;
19319
+ return `${KIND[p.ecosystem]} "${p.name}${version2 ? `@${version2}` : ""}"`;
19320
+ }
19321
+ function osvNames(p) {
19322
+ if (p.ecosystem !== "Go") return [p.name];
19323
+ const parts = p.name.split("/");
19324
+ const out = [];
19325
+ for (let n = parts.length; n >= 2 && out.length < 6; n--) out.push(parts.slice(0, n).join("/"));
19326
+ return out;
18446
19327
  }
18447
19328
  async function maliciousFor(p, version2, cfg, misses, networkCapped) {
18448
- const local = lookupIndex(p.ecosystem, p.name);
18449
- const entries = local.status === "hit" ? local.entries : null;
18450
- if (!entries && !(local.status === "clean" && local.fresh)) {
19329
+ const names = osvNames(p);
19330
+ const local = names.map((n) => lookupIndex(p.ecosystem, n));
19331
+ const hits2 = local.flatMap((l) => l.status === "hit" ? l.entries : []);
19332
+ const entries = hits2.length > 0 ? hits2 : null;
19333
+ const freshClean = local.every((l) => l.status === "clean" && l.fresh);
19334
+ const status = local.some((l) => l.status === "unavailable") ? "unavailable" : local[0].status;
19335
+ if (!entries && !freshClean) {
18451
19336
  if (!cfg.onlineFallback || networkCapped) {
18452
19337
  misses.push(
18453
- `${label(p, version2)}: local index ${local.status}, no online lookup (${networkCapped ? "over the per-command network cap" : "online fallback off"})`
19338
+ `${label(p, version2)}: local index ${status}, no online lookup (${networkCapped ? "over the per-command network cap" : "online fallback off"})`
18454
19339
  );
18455
19340
  return null;
18456
19341
  }
18457
- let ids = null;
19342
+ let perName = null;
18458
19343
  try {
18459
- ids = await queryOsvMalicious([{ ecosystem: p.ecosystem, name: p.name, version: version2 }]);
19344
+ perName = await queryOsvMalicious(
19345
+ names.map((name) => ({ ecosystem: p.ecosystem, name, version: version2 }))
19346
+ );
18460
19347
  } catch (err2) {
18461
19348
  misses.push(`${label(p, version2)}: OSV online ${err2.message}`);
18462
19349
  return null;
18463
19350
  }
19351
+ const ids = perName ? [[...new Set(perName.flat())]] : null;
18464
19352
  if (!ids) {
18465
19353
  misses.push(`${label(p, version2)}: OSV online unavailable`);
18466
19354
  return null;
@@ -18530,7 +19418,7 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
18530
19418
  }
18531
19419
  const findings = [];
18532
19420
  let info = null;
18533
- const wantsRegistry = cfg.newPackage !== "off" || cfg.installScript !== "off";
19421
+ const wantsRegistry = (cfg.newPackage !== "off" || cfg.installScript !== "off") && HAS_REGISTRY_CLIENT.has(p.ecosystem);
18534
19422
  if (wantsRegistry && !networkCapped) {
18535
19423
  try {
18536
19424
  const windowMs = cfg.newPackage === "off" ? 0 : cfg.maxAgeHours * 36e5;
@@ -18610,7 +19498,7 @@ async function runPackageCheck(toolName, args, cfg, cwd) {
18610
19498
  return { ...ALLOW, misses: [`package check error: ${err2?.message ?? err2}`] };
18611
19499
  }
18612
19500
  }
18613
- var import_picomatch3, import_path26, MAX_NETWORK_PACKAGES, ALLOW;
19501
+ var import_picomatch3, import_path26, MAX_NETWORK_PACKAGES, ALLOW, KIND;
18614
19502
  var init_check = __esm({
18615
19503
  "src/supply-chain/check.ts"() {
18616
19504
  "use strict";
@@ -18623,6 +19511,164 @@ var init_check = __esm({
18623
19511
  init_local_resolve();
18624
19512
  MAX_NETWORK_PACKAGES = 10;
18625
19513
  ALLOW = { verdict: "allow", findings: [], misses: [], checked: 0 };
19514
+ KIND = {
19515
+ npm: "npm package",
19516
+ PyPI: "PyPI package",
19517
+ RubyGems: "RubyGems package",
19518
+ "crates.io": "crates.io package",
19519
+ Go: "Go module",
19520
+ NuGet: "NuGet package",
19521
+ Maven: "Maven artifact",
19522
+ Packagist: "Packagist package",
19523
+ VSCode: "VS Code extension"
19524
+ };
19525
+ }
19526
+ });
19527
+
19528
+ // src/policy/negotiation.ts
19529
+ function buildNegotiationMessage(blockedByLabel, isHumanDecision2, humanReason, recoveryCommand, context) {
19530
+ if (isHumanDecision2) {
19531
+ return `NODE9: The human user rejected this action.
19532
+ REASON: ${humanReason || "No specific reason provided."}
19533
+ INSTRUCTIONS:
19534
+ - Do NOT retry this exact command.
19535
+ - Acknowledge the block to the user and ask if there is an alternative approach.
19536
+ - If you believe this action is critical, explain your reasoning and ask them to run "node9 pause 15m" to proceed.`;
19537
+ }
19538
+ if (context?.kind === "protected-path") {
19539
+ const subject = context.path ? `${context.path} is a protected file` : "This file is protected";
19540
+ return `NODE9: Blocked. ${subject}, and node9 stopped this call to it. The call did not run, so nothing was exposed.
19541
+ INSTRUCTIONS:
19542
+ - Do NOT retry, and do NOT reach the file another way (cat, grep, a copy, a script, a redirect).
19543
+ - If this step needs that file, ask the user to provide what you need or to do the step themselves.
19544
+ - Tell the user node9 protected this file, then ask how to proceed.`;
19545
+ }
19546
+ const label3 = blockedByLabel.toLowerCase();
19547
+ if (label3.includes("dlp") || label3.includes("secret detected") || label3.includes("credential review")) {
19548
+ return `NODE9 SECURITY ALERT: A sensitive credential (API key, token, or private key) was found in your tool call arguments.
19549
+ CRITICAL INSTRUCTION: Do NOT retry this action.
19550
+ REQUIRED ACTIONS:
19551
+ 1. Remove the hardcoded credential from your command or code.
19552
+ 2. Use an environment variable or a dedicated secrets manager instead.
19553
+ 3. Treat the leaked credential as compromised and rotate it immediately.
19554
+ Do NOT attempt to bypass this check or pass the credential through another tool.
19555
+ - Tell the user node9 blocked this and why, then ask how to proceed.`;
19556
+ }
19557
+ if (label3.includes("package check")) {
19558
+ return `NODE9: Blocked \u2014 ${humanReason || "a package in this command is known to be malicious."}
19559
+ INSTRUCTIONS:
19560
+ - Do NOT install this package, and do NOT fetch it another way (a different package manager, a tarball URL, a git URL, a mirror).
19561
+ - If a dependency is needed, suggest a well-known alternative and let the user choose.
19562
+ - Tell the user node9 blocked this install and name the advisory, then ask how to proceed.`;
19563
+ }
19564
+ if (label3.includes("sql safety") && label3.includes("delete without where")) {
19565
+ return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
19566
+ INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
19567
+ Do NOT retry without a WHERE clause.
19568
+ - Tell the user node9 blocked this and why, then ask how to proceed.`;
19569
+ }
19570
+ if (label3.includes("sql safety") && label3.includes("update without where")) {
19571
+ return `NODE9: Blocked \u2014 UPDATE without WHERE clause would update every row.
19572
+ INSTRUCTION: Add a WHERE clause to scope the update (e.g. WHERE id = <value>).
19573
+ Do NOT retry without a WHERE clause.
19574
+ - Tell the user node9 blocked this and why, then ask how to proceed.`;
19575
+ }
19576
+ if (label3.includes("dangerous word")) {
19577
+ const match = blockedByLabel.match(/dangerous word: "([^"]+)"/i);
19578
+ const word = match?.[1] ?? "a dangerous keyword";
19579
+ return `NODE9: Blocked \u2014 command contains forbidden keyword "${word}".
19580
+ INSTRUCTION: Do NOT use "${word}". Use a non-destructive alternative.
19581
+ Do NOT attempt to bypass this with shell tricks or aliases \u2014 it will be blocked again.
19582
+ - Tell the user node9 blocked this and why, then ask how to proceed.`;
19583
+ }
19584
+ if (label3.includes("path blocked") || label3.includes("sandbox")) {
19585
+ return `NODE9: Blocked \u2014 operation targets a path outside the allowed sandbox.
19586
+ INSTRUCTION: Move your output to an allowed directory such as /tmp/ or the project directory.
19587
+ Do NOT retry on the same path.
19588
+ - Tell the user node9 blocked this and why, then ask how to proceed.`;
19589
+ }
19590
+ if (label3.includes("inline execution")) {
19591
+ return `NODE9: Blocked \u2014 inline code execution (e.g. bash -c "...") is not allowed.
19592
+ INSTRUCTION: Use individual tool calls instead of embedding code in a shell string.
19593
+ - Tell the user node9 blocked this and why, then ask how to proceed.`;
19594
+ }
19595
+ if (label3.includes("strict mode")) {
19596
+ return `NODE9: Blocked \u2014 strict mode is active. All tool calls require explicit human approval.
19597
+ INSTRUCTION: Inform the user this action is pending approval. Wait for them to approve via the dashboard or run "node9 pause".`;
19598
+ }
19599
+ if (label3.includes("rule") && label3.includes("default block")) {
19600
+ const match = blockedByLabel.match(/rule "([^"]+)"/i);
19601
+ const rule = match?.[1] ?? "a policy rule";
19602
+ return `NODE9: Blocked \u2014 action "${rule}" is forbidden by security policy.
19603
+ INSTRUCTION: Do NOT use "${rule}". Find a read-only or non-destructive alternative.
19604
+ Do NOT attempt to bypass this rule.
19605
+ - Tell the user node9 blocked this and why, then ask how to proceed.`;
19606
+ }
19607
+ const recovery = recoveryCommand ? `
19608
+ REQUIRED ACTION: Run \`${recoveryCommand}\` first, then retry your original command.` : "\n- Pivot to a non-destructive or read-only alternative.";
19609
+ return `NODE9: Action blocked by security policy [${agentFacingLabel(blockedByLabel)}].
19610
+ INSTRUCTIONS:
19611
+ - Do NOT retry this exact command or attempt to bypass the rule.${recovery}
19612
+ - Inform the user which security rule was triggered and ask how to proceed.`;
19613
+ }
19614
+ function buildReviewMessage(blockedByLabel, ruleDescription, reason) {
19615
+ const why = ruleDescription || (reason && reason !== blockedByLabel ? reason : void 0) || blockedByLabel || "this action needs your review";
19616
+ const trimmed = why.trimEnd();
19617
+ const sentence2 = /[.!?\u2026]$/.test(trimmed) ? trimmed : `${trimmed}.`;
19618
+ return `Node9 flagged this for your review: ${sentence2} Approve to proceed, or deny to cancel.`;
19619
+ }
19620
+ function isHumanDecision(label3, blockedBy) {
19621
+ if (blockedBy !== void 0 && blockedBy !== "local-decision") return false;
19622
+ const l = label3.toLowerCase();
19623
+ return l.includes("user") || l.includes("daemon") || l.includes("decision");
19624
+ }
19625
+ function agentFacingLabel(label3) {
19626
+ const plain = label3.replace(/[\p{Extended_Pictographic}\u200D\uFE0F]/gu, "").replace(/\bNode9\b/g, "node9").replace(/\s+/g, " ").trim();
19627
+ return plain || "Security Policy";
19628
+ }
19629
+ function asApprovalRequest(description) {
19630
+ const is = description.lastIndexOf(BLOCKED_BY);
19631
+ if (is > 0) {
19632
+ const by = description.slice(is + BLOCKED_BY.length).replace(/\.$/, "");
19633
+ if (by.endsWith(SHIELD)) return `${description.slice(0, is)} needs your approval (${by})`;
19634
+ }
19635
+ const dash = description.lastIndexOf(DASH_BLOCKED_BY);
19636
+ if (dash > 0) {
19637
+ const by = description.slice(dash + DASH_BLOCKED_BY.length).replace(/\.$/, "");
19638
+ if (by.endsWith(SHIELD)) return `${description.slice(0, dash)}. Needs your approval (${by})`;
19639
+ }
19640
+ return description;
19641
+ }
19642
+ var BLOCKED_BY, DASH_BLOCKED_BY, SHIELD;
19643
+ var init_negotiation = __esm({
19644
+ "src/policy/negotiation.ts"() {
19645
+ "use strict";
19646
+ BLOCKED_BY = " is blocked by ";
19647
+ DASH_BLOCKED_BY = " \u2014 blocked by ";
19648
+ SHIELD = " shield";
19649
+ }
19650
+ });
19651
+
19652
+ // src/auth/decision-sources.ts
19653
+ function backgroundPassSource(result) {
19654
+ if (result.checkedBy === "cloud" || result.blockedBy === "team-policy") return "cloud";
19655
+ if (result.blockedBy === "timeout") return "timeout";
19656
+ return "policy";
19657
+ }
19658
+ var HUMAN_DECISION_SOURCES, REDIRECT_SOURCE, DECISION_ROUTE_SOURCES;
19659
+ var init_decision_sources = __esm({
19660
+ "src/auth/decision-sources.ts"() {
19661
+ "use strict";
19662
+ HUMAN_DECISION_SOURCES = /* @__PURE__ */ new Set([
19663
+ "terminal",
19664
+ "browser",
19665
+ "native"
19666
+ ]);
19667
+ REDIRECT_SOURCE = "terminal-redirect";
19668
+ DECISION_ROUTE_SOURCES = /* @__PURE__ */ new Set([
19669
+ ...HUMAN_DECISION_SOURCES,
19670
+ REDIRECT_SOURCE
19671
+ ]);
18626
19672
  }
18627
19673
  });
18628
19674
 
@@ -18740,6 +19786,10 @@ async function authorizeHeadless(toolName, args, meta, options) {
18740
19786
  }
18741
19787
  return _authorizeHeadlessCore(toolName, args, meta, options);
18742
19788
  }
19789
+ function daemonWaitCapMs(approvalTimeoutMs) {
19790
+ if (approvalTimeoutMs === 0) return 0;
19791
+ return Math.max(approvalTimeoutMs ?? 0, 12e4) + 5e3;
19792
+ }
18743
19793
  function approvalTimeoutReason(approvalTimeoutMs) {
18744
19794
  return `No human response within ${approvalTimeoutMs / 1e3}s \u2014 auto-denied by timeout policy. Approve from your phone next time: run \`node9 login\`.`;
18745
19795
  }
@@ -18775,6 +19825,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
18775
19825
  let statefulRecoveryCommand;
18776
19826
  let localSmartRuleMatched = false;
18777
19827
  let hardBlockDowngraded = false;
19828
+ let downgradedBlock = null;
18778
19829
  let argScan;
18779
19830
  const argumentScan = () => argScan ??= scanArgumentSecrets(args);
18780
19831
  let fileAccess;
@@ -19394,6 +20445,16 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19394
20445
  humanApproverReachable,
19395
20446
  overridable: policyResult.overridable
19396
20447
  });
20448
+ const blocked = {
20449
+ approved: false,
20450
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
20451
+ blockedBy: "local-config",
20452
+ blockedByLabel: policyResult.blockedByLabel,
20453
+ ruleHit: policyResult.ruleName,
20454
+ ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
20455
+ ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription },
20456
+ ...protectedPathFields(policyResult.ruleName, toolName, args, dlpReviewFlagged)
20457
+ };
19397
20458
  const hardBlock = () => {
19398
20459
  if (!isManual)
19399
20460
  appendLocalAudit(
@@ -19404,17 +20465,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19404
20465
  { ...meta, ruleName: policyResult.ruleName },
19405
20466
  hashAuditArgs
19406
20467
  );
19407
- return {
19408
- approved: false,
19409
- reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
19410
- blockedBy: "local-config",
19411
- blockedByLabel: policyResult.blockedByLabel,
19412
- ruleHit: policyResult.ruleName,
19413
- ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
19414
- ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription },
19415
- ...protectedPathFields(policyResult.ruleName, toolName, args, dlpReviewFlagged)
19416
- };
20468
+ return blocked;
19417
20469
  };
20470
+ downgradedBlock = blocked;
19418
20471
  if (policyResult.dependsOnStatePredicates?.length) {
19419
20472
  const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
19420
20473
  const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
@@ -19471,7 +20524,8 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
19471
20524
  policyMatchedWord,
19472
20525
  policyResult.ruleName
19473
20526
  );
19474
- if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
20527
+ if (policyRuleDescription)
20528
+ riskMetadata.ruleDescription = (hardBlockDowngraded ? asApprovalRequest(policyRuleDescription) : policyRuleDescription).slice(0, 200);
19475
20529
  const persistent = policyResult.ruleName || policyResult.tier === 7 || hardBlockDowngraded ? null : getPersistentDecision(toolName);
19476
20530
  if (persistent === "allow" && !callSpecificReview()) {
19477
20531
  if (!isManual)
@@ -19635,7 +20689,7 @@ ${packageReview}`;
19635
20689
  }
19636
20690
  }
19637
20691
  if (initResult.pending) cloudRequestId = initResult.requestId || null;
19638
- if (!callSpecificReview()) explainableLabel = "Organization Policy (SaaS)";
20692
+ if (!reviewDemanded()) explainableLabel = "Organization Policy (SaaS)";
19639
20693
  } catch {
19640
20694
  }
19641
20695
  }
@@ -19777,10 +20831,54 @@ ${packageReview}`;
19777
20831
  decision: daemonDecision,
19778
20832
  source: decisionSource,
19779
20833
  reason: daemonReason
19780
- } = await waitForDaemonDecision(daemonEntryId, signal);
20834
+ } = await waitForDaemonDecision(
20835
+ daemonEntryId,
20836
+ signal,
20837
+ daemonWaitCapMs(config.settings.approvalTimeoutMs)
20838
+ );
19781
20839
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
19782
20840
  const isApproved = daemonDecision === "allow";
19783
- const isRedirect = decisionSource === "terminal-redirect";
20841
+ const isRedirect = decisionSource === REDIRECT_SOURCE;
20842
+ if (!isApproved && !isRedirect && !(decisionSource && HUMAN_DECISION_SOURCES.has(decisionSource))) {
20843
+ if (decisionSource === "cloud")
20844
+ return {
20845
+ approved: false,
20846
+ reason: daemonReason || "Action rejected by organization administrator via Slack.",
20847
+ blockedBy: "team-policy",
20848
+ blockedByLabel: "Organization Policy (SaaS)",
20849
+ decisionSource: "cloud"
20850
+ };
20851
+ const timedOut = decisionSource === "timeout";
20852
+ if (downgradedBlock)
20853
+ return {
20854
+ ...downgradedBlock,
20855
+ blockedBy: timedOut ? "timeout" : "local-config"
20856
+ };
20857
+ if (timedOut)
20858
+ return {
20859
+ approved: false,
20860
+ reason: daemonReason || "No response: auto-denied after the approval timeout.",
20861
+ blockedBy: "timeout",
20862
+ blockedByLabel: "Approval Timeout"
20863
+ };
20864
+ if (decisionSource === "policy")
20865
+ return {
20866
+ approved: false,
20867
+ reason: daemonReason || "Action blocked by policy.",
20868
+ blockedBy: "local-config",
20869
+ blockedByLabel: explainableLabel
20870
+ };
20871
+ return {
20872
+ approved: false,
20873
+ reason: daemonReason || "node9 could not get an approval decision for this call.",
20874
+ blockedBy: "no-approval-mechanism",
20875
+ blockedByLabel: "Approval Unavailable"
20876
+ };
20877
+ }
20878
+ if (isApproved && decisionSource === "cloud")
20879
+ return { approved: true, checkedBy: "cloud", decisionSource: "cloud" };
20880
+ if (isApproved && decisionSource === "policy")
20881
+ return { approved: true, checkedBy: "local-policy" };
19784
20882
  const via = "Terminal (node9 tail)";
19785
20883
  return {
19786
20884
  approved: isApproved,
@@ -19940,6 +21038,8 @@ var init_orchestrator = __esm({
19940
21038
  init_shields();
19941
21039
  init_jail();
19942
21040
  init_safe_text();
21041
+ init_negotiation();
21042
+ init_decision_sources();
19943
21043
  SENSITIVE_PATH_PATTERN = sensitivePathMatch("").patternName;
19944
21044
  WRITE_TOOLS = /* @__PURE__ */ new Set([
19945
21045
  "write",
@@ -30950,23 +32050,22 @@ function checkApprovalConfig(ctx) {
30950
32050
  const s = getConfig(ctx.cwd).settings;
30951
32051
  const a = s.approvers ?? { native: false, browser: false, cloud: false, terminal: false };
30952
32052
  const hasChannel = a.native || a.browser || a.cloud || a.terminal || s.reviewChannel === "ask";
30953
- if (s.approvalTimeoutMs === 0) {
30954
- return [
30955
- {
30956
- category: "Approvals",
30957
- severity: "high",
30958
- title: "Approval timeout is 0 \u2014 every review auto-denies instantly",
30959
- what: "A review request is denied the moment it is raised; no human ever sees it.",
30960
- why: "`settings.approvalTimeoutMs` is 0.",
30961
- who: "Legitimate actions that need a human OK are silently blocked.",
30962
- detail: [],
30963
- owner: "node9",
30964
- fix: "Set a real approval timeout (e.g. 300000 ms) or clear it to use the default."
30965
- }
30966
- ];
30967
- }
32053
+ const zeroTimeout = s.approvalTimeoutMs === 0 ? [
32054
+ {
32055
+ category: "Approvals",
32056
+ severity: "medium",
32057
+ title: "Approval timeout is 0: node9 never times a review out",
32058
+ what: "A review waits for a person; node9's own timer is off.",
32059
+ why: "`settings.approvalTimeoutMs` is 0.",
32060
+ who: "With no one at the screen, the agent's hook timeout ends the review, and some agents then run the tool.",
32061
+ detail: [],
32062
+ owner: "node9",
32063
+ fix: "Set a real approval timeout (e.g. 300000 ms) or clear it to use the default."
32064
+ }
32065
+ ] : [];
30968
32066
  if (!hasChannel) {
30969
32067
  return [
32068
+ ...zeroTimeout,
30970
32069
  {
30971
32070
  category: "Approvals",
30972
32071
  severity: "high",
@@ -30980,7 +32079,7 @@ function checkApprovalConfig(ctx) {
30980
32079
  }
30981
32080
  ];
30982
32081
  }
30983
- return [];
32082
+ return zeroTimeout;
30984
32083
  }
30985
32084
  function checkToolGovernance(ctx) {
30986
32085
  const cfg = getConfig(ctx.cwd);
@@ -33665,7 +34764,17 @@ var init_osv_sync = __esm({
33665
34764
  init_zip();
33666
34765
  init_osv_index();
33667
34766
  init_net();
33668
- ECOSYSTEMS2 = ["npm", "PyPI"];
34767
+ ECOSYSTEMS2 = [
34768
+ "npm",
34769
+ "PyPI",
34770
+ "RubyGems",
34771
+ "crates.io",
34772
+ "Go",
34773
+ "NuGet",
34774
+ "Maven",
34775
+ "Packagist",
34776
+ "VSCode"
34777
+ ];
33669
34778
  SYNC_INTERVAL_MS2 = 6 * 60 * 60 * 1e3;
33670
34779
  FIRST_SYNC_DELAY_MS = 2 * 60 * 1e3;
33671
34780
  FULL_REBUILD_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -34218,6 +35327,15 @@ function buildDaemonReport(allEntries, period, now) {
34218
35327
  byAgent: [...agentMap.values()].sort((a, b) => b.total - a.total)
34219
35328
  };
34220
35329
  }
35330
+ function autoDenyMs() {
35331
+ const ms = getConfig().settings.approvalTimeoutMs;
35332
+ if (ms === 0) return null;
35333
+ return ms ?? AUTO_DENY_MS;
35334
+ }
35335
+ function armAutoDeny(fire) {
35336
+ const ms = autoDenyMs();
35337
+ return ms === null ? void 0 : setTimeout(fire, ms);
35338
+ }
34221
35339
  function startDaemon() {
34222
35340
  try {
34223
35341
  autoMigrateLocalConfig();
@@ -34328,7 +35446,7 @@ data: ${JSON.stringify({
34328
35446
  allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
34329
35447
  })),
34330
35448
  orgName: getOrgName(),
34331
- autoDenyMs: getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS
35449
+ autoDenyMs: autoDenyMs()
34332
35450
  })}
34333
35451
 
34334
35452
  `
@@ -34391,7 +35509,7 @@ data: ${JSON.stringify(item.data)}
34391
35509
  timestamp: Date.now(),
34392
35510
  earlyDecision: null,
34393
35511
  waiter: null,
34394
- timer: setTimeout(() => {
35512
+ timer: armAutoDeny(() => {
34395
35513
  if (pending.has(id)) {
34396
35514
  const e = pending.get(id);
34397
35515
  appendAuditLog({
@@ -34399,6 +35517,7 @@ data: ${JSON.stringify(item.data)}
34399
35517
  args: e.args,
34400
35518
  decision: "auto-deny"
34401
35519
  });
35520
+ e.decisionSource = "timeout";
34402
35521
  if (e.waiter) e.waiter("deny", "No response \u2014 auto-denied after timeout");
34403
35522
  else {
34404
35523
  e.earlyDecision = "deny";
@@ -34407,7 +35526,7 @@ data: ${JSON.stringify(item.data)}
34407
35526
  pending.delete(id);
34408
35527
  broadcast("remove", { id, decision: "deny" });
34409
35528
  }
34410
- }, getConfig().settings.approvalTimeoutMs ?? AUTO_DENY_MS)
35529
+ })
34411
35530
  };
34412
35531
  pending.set(id, entry);
34413
35532
  if (!fromCLI) {
@@ -34469,6 +35588,7 @@ data: ${JSON.stringify(item.data)}
34469
35588
  clearTimeout(e.timer);
34470
35589
  const decision = result.approved ? "allow" : "deny";
34471
35590
  appendAuditLog({ toolName: e.toolName, args: e.args, decision });
35591
+ e.decisionSource = backgroundPassSource(result);
34472
35592
  if (e.waiter) {
34473
35593
  e.waiter(decision, result.reason);
34474
35594
  pending.delete(id);
@@ -34583,8 +35703,7 @@ data: ${JSON.stringify(item.data)}
34583
35703
  insightCounts.delete(entry.toolName);
34584
35704
  saveInsightCounts();
34585
35705
  }
34586
- const VALID_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native", "terminal-redirect"]);
34587
- if (source && VALID_SOURCES.has(source)) entry.decisionSource = source;
35706
+ if (source && DECISION_ROUTE_SOURCES.has(source)) entry.decisionSource = source;
34588
35707
  if (entry.waiter) {
34589
35708
  entry.waiter(resolvedDecision, reason);
34590
35709
  pending.delete(id);
@@ -34722,8 +35841,7 @@ data: ${JSON.stringify(item.data)}
34722
35841
  insightCounts.delete(entry.toolName);
34723
35842
  saveInsightCounts();
34724
35843
  }
34725
- const VALID_RESOLVE_SOURCES = /* @__PURE__ */ new Set(["terminal", "browser", "native"]);
34726
- if (source && VALID_RESOLVE_SOURCES.has(source)) entry.decisionSource = source;
35844
+ if (source && HUMAN_DECISION_SOURCES.has(source)) entry.decisionSource = source;
34727
35845
  if (entry.waiter) entry.waiter(resolvedResolveDecision);
34728
35846
  else entry.earlyDecision = resolvedResolveDecision;
34729
35847
  pending.delete(id);
@@ -35387,6 +36505,7 @@ var init_server = __esm({
35387
36505
  import_os50 = __toESM(require("os"));
35388
36506
  import_crypto18 = require("crypto");
35389
36507
  init_trusted_recipients();
36508
+ init_decision_sources();
35390
36509
  import_child_process5 = require("child_process");
35391
36510
  import_chalk7 = __toESM(require("chalk"));
35392
36511
  init_core();
@@ -35645,14 +36764,14 @@ async function onboardMachine(apiKey, opts = {}) {
35645
36764
  detail: `${sync.rules} cloud rule${sync.rules === 1 ? "" : "s"} active`
35646
36765
  } : { name: "policy-sync", ok: false, detail: sync.reason }
35647
36766
  );
35648
- const push = await runPolicyPush();
36767
+ const push2 = await runPolicyPush();
35649
36768
  steps.push(
35650
- push.ok ? { name: "register", ok: true, detail: "machine visible in the dashboard" } : { name: "register", ok: false, detail: push.reason }
36769
+ push2.ok ? { name: "register", ok: true, detail: "machine visible in the dashboard" } : { name: "register", ok: false, detail: push2.reason }
35651
36770
  );
35652
36771
  const healed = ensureAutostartHealthy(!!getConfig().settings.autoStartDaemon);
35653
36772
  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)";
35654
36773
  let daemonOk = true;
35655
- if (sync.ok && push.ok && isInteractive() && mayChangeService({ stdoutTTY: !!process.stdout.isTTY, ci: isCI() }) && autostartState() === "absent") {
36774
+ if (sync.ok && push2.ok && isInteractive() && mayChangeService({ stdoutTTY: !!process.stdout.isTTY, ci: isCI() }) && autostartState() === "absent") {
35656
36775
  try {
35657
36776
  if (await (0, import_prompts2.confirm)({
35658
36777
  message: "Run node9 in the background when this computer starts?",
@@ -63931,101 +65050,7 @@ var import_child_process6 = require("child_process");
63931
65050
  var import_execa = require("execa");
63932
65051
  var import_execa2 = require("execa");
63933
65052
  init_orchestrator();
63934
-
63935
- // src/policy/negotiation.ts
63936
- function buildNegotiationMessage(blockedByLabel, isHumanDecision, humanReason, recoveryCommand, context) {
63937
- if (isHumanDecision) {
63938
- return `NODE9: The human user rejected this action.
63939
- REASON: ${humanReason || "No specific reason provided."}
63940
- INSTRUCTIONS:
63941
- - Do NOT retry this exact command.
63942
- - Acknowledge the block to the user and ask if there is an alternative approach.
63943
- - If you believe this action is critical, explain your reasoning and ask them to run "node9 pause 15m" to proceed.`;
63944
- }
63945
- if (context?.kind === "protected-path") {
63946
- const subject = context.path ? `${context.path} is a protected file` : "This file is protected";
63947
- return `NODE9: Blocked. ${subject}, and node9 stopped this call to it. The call did not run, so nothing was exposed.
63948
- INSTRUCTIONS:
63949
- - Do NOT retry, and do NOT reach the file another way (cat, grep, a copy, a script, a redirect).
63950
- - If this step needs that file, ask the user to provide what you need or to do the step themselves.
63951
- - Tell the user node9 protected this file, then ask how to proceed.`;
63952
- }
63953
- const label3 = blockedByLabel.toLowerCase();
63954
- if (label3.includes("dlp") || label3.includes("secret detected") || label3.includes("credential review")) {
63955
- return `NODE9 SECURITY ALERT: A sensitive credential (API key, token, or private key) was found in your tool call arguments.
63956
- CRITICAL INSTRUCTION: Do NOT retry this action.
63957
- REQUIRED ACTIONS:
63958
- 1. Remove the hardcoded credential from your command or code.
63959
- 2. Use an environment variable or a dedicated secrets manager instead.
63960
- 3. Treat the leaked credential as compromised and rotate it immediately.
63961
- Do NOT attempt to bypass this check or pass the credential through another tool.
63962
- - Tell the user node9 blocked this and why, then ask how to proceed.`;
63963
- }
63964
- if (label3.includes("package check")) {
63965
- return `NODE9: Blocked \u2014 ${humanReason || "a package in this command is known to be malicious."}
63966
- INSTRUCTIONS:
63967
- - Do NOT install this package, and do NOT fetch it another way (a different package manager, a tarball URL, a git URL, a mirror).
63968
- - If a dependency is needed, suggest a well-known alternative and let the user choose.
63969
- - Tell the user node9 blocked this install and name the advisory, then ask how to proceed.`;
63970
- }
63971
- if (label3.includes("sql safety") && label3.includes("delete without where")) {
63972
- return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
63973
- INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
63974
- Do NOT retry without a WHERE clause.
63975
- - Tell the user node9 blocked this and why, then ask how to proceed.`;
63976
- }
63977
- if (label3.includes("sql safety") && label3.includes("update without where")) {
63978
- return `NODE9: Blocked \u2014 UPDATE without WHERE clause would update every row.
63979
- INSTRUCTION: Add a WHERE clause to scope the update (e.g. WHERE id = <value>).
63980
- Do NOT retry without a WHERE clause.
63981
- - Tell the user node9 blocked this and why, then ask how to proceed.`;
63982
- }
63983
- if (label3.includes("dangerous word")) {
63984
- const match = blockedByLabel.match(/dangerous word: "([^"]+)"/i);
63985
- const word = match?.[1] ?? "a dangerous keyword";
63986
- return `NODE9: Blocked \u2014 command contains forbidden keyword "${word}".
63987
- INSTRUCTION: Do NOT use "${word}". Use a non-destructive alternative.
63988
- Do NOT attempt to bypass this with shell tricks or aliases \u2014 it will be blocked again.
63989
- - Tell the user node9 blocked this and why, then ask how to proceed.`;
63990
- }
63991
- if (label3.includes("path blocked") || label3.includes("sandbox")) {
63992
- return `NODE9: Blocked \u2014 operation targets a path outside the allowed sandbox.
63993
- INSTRUCTION: Move your output to an allowed directory such as /tmp/ or the project directory.
63994
- Do NOT retry on the same path.
63995
- - Tell the user node9 blocked this and why, then ask how to proceed.`;
63996
- }
63997
- if (label3.includes("inline execution")) {
63998
- return `NODE9: Blocked \u2014 inline code execution (e.g. bash -c "...") is not allowed.
63999
- INSTRUCTION: Use individual tool calls instead of embedding code in a shell string.
64000
- - Tell the user node9 blocked this and why, then ask how to proceed.`;
64001
- }
64002
- if (label3.includes("strict mode")) {
64003
- return `NODE9: Blocked \u2014 strict mode is active. All tool calls require explicit human approval.
64004
- INSTRUCTION: Inform the user this action is pending approval. Wait for them to approve via the dashboard or run "node9 pause".`;
64005
- }
64006
- if (label3.includes("rule") && label3.includes("default block")) {
64007
- const match = blockedByLabel.match(/rule "([^"]+)"/i);
64008
- const rule = match?.[1] ?? "a policy rule";
64009
- return `NODE9: Blocked \u2014 action "${rule}" is forbidden by security policy.
64010
- INSTRUCTION: Do NOT use "${rule}". Find a read-only or non-destructive alternative.
64011
- Do NOT attempt to bypass this rule.
64012
- - Tell the user node9 blocked this and why, then ask how to proceed.`;
64013
- }
64014
- const recovery = recoveryCommand ? `
64015
- REQUIRED ACTION: Run \`${recoveryCommand}\` first, then retry your original command.` : "\n- Pivot to a non-destructive or read-only alternative.";
64016
- return `NODE9: Action blocked by security policy [${blockedByLabel}].
64017
- INSTRUCTIONS:
64018
- - Do NOT retry this exact command or attempt to bypass the rule.${recovery}
64019
- - Inform the user which security rule was triggered and ask how to proceed.`;
64020
- }
64021
- function buildReviewMessage(blockedByLabel, ruleDescription, reason) {
64022
- const why = ruleDescription || (reason && reason !== blockedByLabel ? reason : void 0) || blockedByLabel || "this action needs your review";
64023
- const trimmed = why.trimEnd();
64024
- const sentence2 = /[.!?\u2026]$/.test(trimmed) ? trimmed : `${trimmed}.`;
64025
- return `Node9 flagged this for your review: ${sentence2} Approve to proceed, or deny to cancel.`;
64026
- }
64027
-
64028
- // src/proxy/index.ts
65053
+ init_negotiation();
64029
65054
  init_platform_shell();
64030
65055
  init_safe_text();
64031
65056
  async function runProxy(targetCommand) {
@@ -64073,7 +65098,7 @@ async function runProxy(targetCommand) {
64073
65098
  console.error(import_chalk9.default.gray(` Reason: ${result.reason || "Security Policy"}
64074
65099
  `));
64075
65100
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
64076
- const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
65101
+ const isHuman = isHumanDecision(blockedByLabel, result.blockedBy);
64077
65102
  const aiInstruction = buildNegotiationMessage(
64078
65103
  blockedByLabel,
64079
65104
  isHuman,
@@ -64209,6 +65234,7 @@ init_daemon();
64209
65234
  init_dist();
64210
65235
  init_startup_log();
64211
65236
  init_config();
65237
+ init_negotiation();
64212
65238
  init_daemon_starter();
64213
65239
 
64214
65240
  // src/skill-pin.ts
@@ -64754,7 +65780,7 @@ RAW: ${raw}
64754
65780
  const mcpServer = mcpMatch?.[1];
64755
65781
  const sendBlock = (msg, result2) => {
64756
65782
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || (isKeyedForPolicy() ? "Workspace Policy" : "Local Security Policy");
64757
- const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
65783
+ const humanDecision = isHumanDecision(blockedByContext, result2?.blockedBy);
64758
65784
  let ttyFd = null;
64759
65785
  try {
64760
65786
  ttyFd = import_fs66.default.openSync("/dev/tty", "w");
@@ -64791,7 +65817,7 @@ RAW: ${raw}
64791
65817
  }
64792
65818
  const aiFeedbackMessage = buildNegotiationMessage(
64793
65819
  blockedByContext,
64794
- isHumanDecision,
65820
+ humanDecision,
64795
65821
  msg,
64796
65822
  result2?.recoveryCommand,
64797
65823
  { kind: result2?.blockKind, path: result2?.blockedPath }
@@ -65199,7 +66225,7 @@ function collectToolOutputText(toolResponse, opts = {}) {
65199
66225
  let size = 0;
65200
66226
  let fields = 0;
65201
66227
  let truncated = false;
65202
- const push = (s) => {
66228
+ const push2 = (s) => {
65203
66229
  if (s.length === 0) return true;
65204
66230
  const room = maxBytes - size;
65205
66231
  if (s.length > room) {
@@ -65217,7 +66243,7 @@ function collectToolOutputText(toolResponse, opts = {}) {
65217
66243
  return true;
65218
66244
  };
65219
66245
  const walk2 = (v, depth) => {
65220
- if (typeof v === "string") return push(v);
66246
+ if (typeof v === "string") return push2(v);
65221
66247
  if (depth >= MAX_DEPTH5 || v === null || typeof v !== "object") return true;
65222
66248
  const children = Array.isArray(v) ? v : Object.values(v);
65223
66249
  for (const child of children) if (!walk2(child, depth + 1)) return false;
@@ -68384,6 +69410,7 @@ init_orchestrator();
68384
69410
  init_config();
68385
69411
  init_cloud();
68386
69412
  init_config();
69413
+ init_negotiation();
68387
69414
  init_provenance();
68388
69415
  init_mcp_pin();
68389
69416
  init_mcp_cmd();
@@ -68753,10 +69780,10 @@ async function runMcpGateway(upstreamCommand, configName) {
68753
69780
  console.error(import_chalk24.default.gray(` Reason: ${result.reason ?? "Security Policy"}
68754
69781
  `));
68755
69782
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
68756
- const isHumanDecision = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
69783
+ const humanDecision = isHumanDecision(blockedByLabel, result.blockedBy);
68757
69784
  const aiInstruction = buildNegotiationMessage(
68758
69785
  blockedByLabel,
68759
- isHumanDecision,
69786
+ humanDecision,
68760
69787
  result.reason,
68761
69788
  void 0,
68762
69789
  { kind: result.blockKind, path: result.blockedPath }
@@ -75843,7 +76870,7 @@ function addHost(list, host2) {
75843
76870
  if (!cliGuardPolicyWrite(`egress ${list} ${host2}`)) return false;
75844
76871
  return guard(() => addEgressHost(list, host2));
75845
76872
  }
75846
- function exempt(address) {
76873
+ function exempt2(address) {
75847
76874
  if (!cliGuardPolicyWrite(`egress exempt ${address}`)) return false;
75848
76875
  return guard(() => addSsrfExemption(address));
75849
76876
  }
@@ -76026,7 +77053,7 @@ function registerEgressCommand(program2) {
76026
77053
  process.exitCode = 1;
76027
77054
  return;
76028
77055
  }
76029
- if (!exempt(a)) return;
77056
+ if (!exempt2(a)) return;
76030
77057
  console.log(import_chalk34.default.green(`
76031
77058
  \u2713 ${a} is exempt from the floor.`));
76032
77059
  if (!m)
@@ -77815,17 +78842,18 @@ function registerSessionExposureCommand(program2) {
77815
78842
  var import_chalk42 = __toESM(require("chalk"));
77816
78843
  init_osv_index();
77817
78844
  init_osv_sync();
78845
+ var ECO_WIDTH = Math.max(...ECOSYSTEMS2.map((e) => e.length));
77818
78846
  function registerPackageIndexCommand(program2) {
77819
78847
  const cmd = program2.command("package-index").description("Local OSV malicious-package index used to check installs before they run");
77820
78848
  cmd.command("status").description("Show when each ecosystem was last synced and how many records it holds").action(() => {
77821
78849
  for (const eco of ECOSYSTEMS2) {
77822
78850
  const meta = readMeta(eco);
77823
78851
  if (!meta) {
77824
- console.log(`${eco.padEnd(5)} ${import_chalk42.default.yellow("not synced")}`);
78852
+ console.log(`${eco.padEnd(ECO_WIDTH)} ${import_chalk42.default.yellow("not synced")}`);
77825
78853
  continue;
77826
78854
  }
77827
78855
  console.log(
77828
- `${eco.padEnd(5)} ${meta.records} records synced ${meta.syncedAt} (${meta.mode})`
78856
+ `${eco.padEnd(ECO_WIDTH)} ${meta.records} records synced ${meta.syncedAt} (${meta.mode})`
77829
78857
  );
77830
78858
  }
77831
78859
  });
@@ -77835,9 +78863,9 @@ function registerPackageIndexCommand(program2) {
77835
78863
  for (const r of results) {
77836
78864
  if (r.error) {
77837
78865
  failed = true;
77838
- console.error(`${r.ecosystem.padEnd(5)} ${import_chalk42.default.red("failed")}: ${r.error}`);
78866
+ console.error(`${r.ecosystem.padEnd(ECO_WIDTH)} ${import_chalk42.default.red("failed")}: ${r.error}`);
77839
78867
  } else {
77840
- console.log(`${r.ecosystem.padEnd(5)} ${r.mode}: ${r.records} records`);
78868
+ console.log(`${r.ecosystem.padEnd(ECO_WIDTH)} ${r.mode}: ${r.records} records`);
77841
78869
  }
77842
78870
  }
77843
78871
  process.exitCode = failed ? 1 : 0;