@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/index.js CHANGED
@@ -1183,6 +1183,120 @@ function expandPlainParam(p) {
1183
1183
  if (recorded !== void 0) return recorded;
1184
1184
  return HOME_VARIABLES.has(p.Param?.Value) ? "~" : void 0;
1185
1185
  }
1186
+ function functionBodies(f) {
1187
+ const out = /* @__PURE__ */ new Map();
1188
+ syntax.Walk(f, (node) => {
1189
+ const n = node;
1190
+ if (n && syntax.NodeType(n) === "FuncDecl" && n.Name?.Value && n.Body) {
1191
+ const list = out.get(n.Name.Value) ?? [];
1192
+ list.push(n.Body);
1193
+ out.set(n.Name.Value, list);
1194
+ }
1195
+ return true;
1196
+ });
1197
+ return out;
1198
+ }
1199
+ function rebindsPositionals(body) {
1200
+ let found = false;
1201
+ syntax.Walk(body, (node) => {
1202
+ const n = node;
1203
+ if (found || !n) return false;
1204
+ if (syntax.NodeType(n) === "CallExpr") {
1205
+ const words = (n.Args || []).map((a) => resolveWordLiteral(a));
1206
+ if (words[0] === "shift") found = true;
1207
+ else if (words[0] === "set" && words.slice(1).some((w) => w === null || w === "--" || !/^[-+]/.test(w)))
1208
+ found = true;
1209
+ }
1210
+ return !found;
1211
+ });
1212
+ return found;
1213
+ }
1214
+ function tableBindings() {
1215
+ if (!assignmentTable) return "";
1216
+ const out = [];
1217
+ for (const [name, rec] of assignmentTable)
1218
+ if (rec.at < currentStmtOffset) out.push(`${name}=${rec.value ?? "\0"}`);
1219
+ return out.sort().join("");
1220
+ }
1221
+ function walkWithCalls(f, visit) {
1222
+ const bodies = functionBodies(f);
1223
+ if (bodies.size === 0) {
1224
+ syntax.Walk(f, visit);
1225
+ return;
1226
+ }
1227
+ const seenCalls = /* @__PURE__ */ new Set();
1228
+ const inline = (call) => {
1229
+ const words = (call.Args || []).map((a) => resolveWordLiteral(a));
1230
+ const list = words[0] ? bodies.get(words[0]) : void 0;
1231
+ if (!list || callDepth >= MAX_CALL_DEPTH) return;
1232
+ const args = words.slice(1);
1233
+ const key = `${words[0]}\0${JSON.stringify(args)}\0${tableBindings()}`;
1234
+ if (seenCalls.has(key)) return;
1235
+ seenCalls.add(key);
1236
+ const outerTable = assignmentTable;
1237
+ const outerOffset = currentStmtOffset;
1238
+ for (const body of list) {
1239
+ if (callWalks >= MAX_CALL_WALKS) return;
1240
+ callWalks++;
1241
+ const table = /* @__PURE__ */ new Map();
1242
+ for (const [name, rec] of outerTable ?? [])
1243
+ if (rec.at < outerOffset) table.set(name, { value: rec.value, at: -1 });
1244
+ for (const name of ["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*", "#"])
1245
+ table.delete(name);
1246
+ if (!rebindsPositionals(body)) {
1247
+ args.slice(0, 9).forEach((value, i) => table.set(String(i + 1), { value, at: -1 }));
1248
+ if (args.length === 1) {
1249
+ table.set("@", { value: args[0], at: -1 });
1250
+ table.set("*", { value: args[0], at: -1 });
1251
+ }
1252
+ table.set("#", { value: String(args.length), at: -1 });
1253
+ }
1254
+ assignmentTable = table;
1255
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1256
+ callDepth++;
1257
+ try {
1258
+ if (body.Cmd && syntax.NodeType(body.Cmd) === "Block") recordTopLevelAssignments(body.Cmd);
1259
+ syntax.Walk(body, step);
1260
+ } finally {
1261
+ callDepth--;
1262
+ assignmentTable = outerTable;
1263
+ currentStmtOffset = outerOffset;
1264
+ }
1265
+ }
1266
+ };
1267
+ const step = (node) => {
1268
+ if (!node) return false;
1269
+ const go = visit(node);
1270
+ if (go !== false && syntax.NodeType(node) === "CallExpr") inline(node);
1271
+ return go;
1272
+ };
1273
+ syntax.Walk(f, step);
1274
+ }
1275
+ function withAssignments(f, body) {
1276
+ const outerTable = assignmentTable;
1277
+ const outerOffset = currentStmtOffset;
1278
+ const outerWalks = callWalks;
1279
+ callWalks = 0;
1280
+ assignmentTable = new Map(
1281
+ [...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
1282
+ );
1283
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1284
+ try {
1285
+ recordTopLevelAssignments(f);
1286
+ return body();
1287
+ } finally {
1288
+ assignmentTable = outerTable;
1289
+ currentStmtOffset = outerOffset;
1290
+ callWalks = outerWalks;
1291
+ }
1292
+ }
1293
+ function trackStatement(n) {
1294
+ try {
1295
+ currentStmtOffset = n.Pos().Offset();
1296
+ } catch {
1297
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1298
+ }
1299
+ }
1186
1300
  function resolveWordLiteral(w) {
1187
1301
  const parts = w?.Parts || [];
1188
1302
  let s = "";
@@ -1303,27 +1417,31 @@ function extractShellDestinations(command) {
1303
1417
  const out = [];
1304
1418
  const seen = /* @__PURE__ */ new Set();
1305
1419
  try {
1306
- syntax.Walk(f, (node) => {
1307
- if (!node) return false;
1308
- const n = node;
1309
- if (syntax.NodeType(n) !== "CallExpr") return true;
1310
- const callArgs = n.Args || [];
1311
- if (callArgs.length === 0) return true;
1312
- const words = callArgs.map((a) => resolveWordLiteral(a));
1313
- const at = unwrapCommandHead(words);
1314
- const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
1315
- if (!NET_BINARIES.has(name)) return true;
1316
- const rest = words.slice(at + 1);
1317
- for (const raw of destTokensForBinary(name, rest)) {
1318
- const host2 = parseDestHost(raw);
1319
- if (!host2) continue;
1320
- const key = `${name}:${host2}`;
1321
- if (seen.has(key)) continue;
1322
- seen.add(key);
1323
- out.push({ host: host2, binary: name, raw });
1324
- }
1325
- return true;
1326
- });
1420
+ withAssignments(
1421
+ f,
1422
+ () => walkWithCalls(f, (node) => {
1423
+ if (!node) return false;
1424
+ const n = node;
1425
+ if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1426
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1427
+ const callArgs = n.Args || [];
1428
+ if (callArgs.length === 0) return true;
1429
+ const words = callArgs.map((a) => resolveWordLiteral(a));
1430
+ const at = unwrapCommandHead(words);
1431
+ const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
1432
+ if (!NET_BINARIES.has(name)) return true;
1433
+ const rest = words.slice(at + 1);
1434
+ for (const raw of destTokensForBinary(name, rest)) {
1435
+ const host2 = parseDestHost(raw);
1436
+ if (!host2) continue;
1437
+ const key = `${name}:${host2}`;
1438
+ if (seen.has(key)) continue;
1439
+ seen.add(key);
1440
+ out.push({ host: host2, binary: name, raw });
1441
+ }
1442
+ return true;
1443
+ })
1444
+ );
1327
1445
  } catch {
1328
1446
  return out;
1329
1447
  }
@@ -1335,39 +1453,45 @@ function extractShellDestTokens(command) {
1335
1453
  const out = [];
1336
1454
  const seen = /* @__PURE__ */ new Set();
1337
1455
  try {
1338
- syntax.Walk(f, (node) => {
1339
- if (!node) return false;
1340
- const n = node;
1341
- if (syntax.NodeType(n) !== "CallExpr") return true;
1342
- const callArgs = n.Args || [];
1343
- if (callArgs.length === 0) return true;
1344
- const name = (resolveWordLiteral(callArgs[0]) || "").toLowerCase();
1345
- if (!NET_BINARIES.has(name)) return true;
1346
- const rest = callArgs.slice(1).map((a) => resolveWordLiteral(a));
1347
- for (const raw of destTokensForBinary(name, rest)) {
1348
- if (!raw) continue;
1349
- let tok = raw.trim();
1350
- const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(tok);
1351
- const hasScheme = scheme !== null;
1352
- if (hasScheme) tok = tok.slice(scheme[0].length);
1353
- tok = tok.split(/[/?#]/)[0];
1354
- const at = tok.lastIndexOf("@");
1355
- if (at >= 0) tok = tok.slice(at + 1);
1356
- if (tok.startsWith("[")) {
1357
- const close = tok.indexOf("]");
1358
- if (close > 0) tok = tok.slice(0, close + 1);
1359
- } else {
1360
- tok = tok.split(":")[0];
1456
+ withAssignments(
1457
+ f,
1458
+ () => walkWithCalls(f, (node) => {
1459
+ if (!node) return false;
1460
+ const n = node;
1461
+ if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1462
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1463
+ const callArgs = n.Args || [];
1464
+ if (callArgs.length === 0) return true;
1465
+ const words = callArgs.map((a) => resolveWordLiteral(a));
1466
+ const head = unwrapCommandHead(words);
1467
+ const name = (words[head] || "").toLowerCase().split("/").pop() ?? "";
1468
+ if (!NET_BINARIES.has(name)) return true;
1469
+ const rest = words.slice(head + 1);
1470
+ for (const raw of destTokensForBinary(name, rest)) {
1471
+ if (!raw) continue;
1472
+ let tok = raw.trim();
1473
+ const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(tok);
1474
+ const hasScheme = scheme !== null;
1475
+ if (hasScheme) tok = tok.slice(scheme[0].length);
1476
+ tok = tok.split(/[/?#]/)[0];
1477
+ const at = tok.lastIndexOf("@");
1478
+ if (at >= 0) tok = tok.slice(at + 1);
1479
+ if (tok.startsWith("[")) {
1480
+ const close = tok.indexOf("]");
1481
+ if (close > 0) tok = tok.slice(0, close + 1);
1482
+ } else {
1483
+ tok = tok.split(":")[0];
1484
+ }
1485
+ if (!tok) continue;
1486
+ if (!hasScheme && /^\d+$/.test(tok) && Number(tok) < 16777216) continue;
1487
+ const key = `${name}:${tok}`;
1488
+ if (seen.has(key)) continue;
1489
+ seen.add(key);
1490
+ out.push({ token: tok, binary: name });
1361
1491
  }
1362
- if (!tok) continue;
1363
- if (!hasScheme && /^\d+$/.test(tok) && Number(tok) < 16777216) continue;
1364
- const key = `${name}:${tok}`;
1365
- if (seen.has(key)) continue;
1366
- seen.add(key);
1367
- out.push({ token: tok, binary: name });
1368
- }
1369
- return true;
1370
- });
1492
+ return true;
1493
+ })
1494
+ );
1371
1495
  } catch {
1372
1496
  return out;
1373
1497
  }
@@ -1486,6 +1610,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1486
1610
  if (depth === 0) {
1487
1611
  seenPayloads = /* @__PURE__ */ new Set();
1488
1612
  payloadBudget = PAYLOAD_BUDGET;
1613
+ if (callDepth === 0) callWalks = 0;
1489
1614
  }
1490
1615
  assignmentTable = new Map(
1491
1616
  [...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
@@ -1493,7 +1618,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1493
1618
  currentStmtOffset = Number.MAX_SAFE_INTEGER;
1494
1619
  try {
1495
1620
  recordTopLevelAssignments(f);
1496
- syntax.Walk(f, (node) => {
1621
+ walkWithCalls(f, (node) => {
1497
1622
  if (!node || result?.verdict === "block") return false;
1498
1623
  const n = node;
1499
1624
  const nodeType = syntax.NodeType(n);
@@ -2282,6 +2407,19 @@ function runnablePrefix(command) {
2282
2407
  }
2283
2408
  return null;
2284
2409
  }
2410
+ function uploadedFile(tool, flag, value) {
2411
+ const v = value.value;
2412
+ const file2 = (f) => f && f !== "-" && f !== "." ? { value: f } : null;
2413
+ if (tool === "curl") {
2414
+ if (["-d", "--data", "--data-ascii", "--data-binary", "--json"].includes(flag))
2415
+ return v.startsWith("@") ? file2(v.slice(1)) : null;
2416
+ if (flag === "--data-urlencode") return file2(/^[^=@]*@(.+)$/.exec(v)?.[1]);
2417
+ if (flag === "-F" || flag === "--form") return file2(/^[^=]*=[@<]([^;]+)/.exec(v)?.[1]);
2418
+ if (flag === "-T" || flag === "--upload-file") return file2(v);
2419
+ }
2420
+ if (tool === "wget" && ["--post-file", "--body-file"].includes(flag)) return file2(v);
2421
+ return null;
2422
+ }
2285
2423
  function merge(a, b) {
2286
2424
  a.cwd = unique([...a.cwd, ...b.cwd]);
2287
2425
  for (const key of /* @__PURE__ */ new Set([...a.vars.keys(), ...b.vars.keys()])) {
@@ -2355,6 +2493,9 @@ function wordValues(word, state) {
2355
2493
  function collectShellFileAccesses(command, context = {}) {
2356
2494
  const out = [];
2357
2495
  let payloads = 256;
2496
+ const functions = /* @__PURE__ */ new Map();
2497
+ const seenCalls = /* @__PURE__ */ new Set();
2498
+ let callWalks2 = MAX_CALL_WALKS2;
2358
2499
  const initial = {
2359
2500
  cwd: [context.cwd ?? ""],
2360
2501
  vars: /* @__PURE__ */ new Map([["HOME", [context.homeDirs?.[0] ?? "~"]]])
@@ -2612,8 +2753,98 @@ function collectShellFileAccesses(command, context = {}) {
2612
2753
  }
2613
2754
  return;
2614
2755
  }
2756
+ if (name === "wget" || name === "curl" || name === "aria2c") {
2757
+ downloadOutputs(name, args, state);
2758
+ return;
2759
+ }
2760
+ if (["http", "https", "xh", "xhs"].includes(name)) {
2761
+ for (const token of args) {
2762
+ if (!token || token.dynamic || /^[a-z][a-z0-9+.-]*:\/\//i.test(token.value)) continue;
2763
+ const m = /^(?:@|[^=:@\s]+@|[^=:@\s]*=@)(.+)$/.exec(token.value);
2764
+ if (m && m[1] !== "-") add("read", { value: m[1] }, state, { upload: true });
2765
+ }
2766
+ return;
2767
+ }
2768
+ if (name === "scp" || name === "rsync") {
2769
+ const operands2 = strings.filter((s) => s && !s.startsWith("-"));
2770
+ const remote = operands2.slice(1).some((o) => /^[^/\s]+:/.test(o));
2771
+ for (const p of copySourcePaths(words))
2772
+ add("read", locate(p), state, remote ? { upload: true } : {});
2773
+ return;
2774
+ }
2615
2775
  for (const p of copySourcePaths(words)) add("read", locate(p), state);
2616
2776
  }
2777
+ function downloadOutputs(name, args, state) {
2778
+ const valueShort = DOWNLOAD_SHORT_VALUE[name];
2779
+ const outputs = [];
2780
+ const urls = [];
2781
+ let dir2 = null;
2782
+ let remoteName = false;
2783
+ let document = false;
2784
+ let serverNamed = false;
2785
+ const take = (flag, value) => {
2786
+ if (!value) return;
2787
+ const sent = uploadedFile(name, flag, value);
2788
+ if (sent) add("read", sent, state, { upload: true });
2789
+ if (DOWNLOAD_OUTPUT_FLAGS[name].includes(flag)) {
2790
+ if (["-O", "--output-document", "--out"].includes(flag) || name === "aria2c" && flag === "-o")
2791
+ document = true;
2792
+ if (value.value !== "-" || value.pattern || value.dynamic) outputs.push(value);
2793
+ } else if (DOWNLOAD_DIR_FLAGS[name].includes(flag)) dir2 = value;
2794
+ else if (name === "wget" && (flag === "-i" || flag === "--input-file")) serverNamed = true;
2795
+ };
2796
+ for (let i = 0; i < args.length; i++) {
2797
+ const token = args[i];
2798
+ if (!token) continue;
2799
+ const v = token.value;
2800
+ if (v.startsWith("--")) {
2801
+ const eq = v.indexOf("=");
2802
+ const flag = eq < 0 ? v : v.slice(0, eq);
2803
+ if (["--remote-name", "--remote-name-all"].includes(flag)) remoteName = true;
2804
+ if (["--remote-header-name", "--recursive", "--mirror", "--page-requisites"].includes(flag))
2805
+ serverNamed = true;
2806
+ if (DOWNLOAD_LONG_VALUE[name].includes(flag))
2807
+ take(flag, eq < 0 ? args[++i] ?? null : { ...token, value: v.slice(eq + 1) });
2808
+ continue;
2809
+ }
2810
+ if (v.startsWith("-") && v.length > 1 && !token.dynamic) {
2811
+ for (let j = 1; j < v.length; j++) {
2812
+ const c = v[j];
2813
+ if (name === "wget" && c === "n") {
2814
+ j++;
2815
+ continue;
2816
+ }
2817
+ if (name === "curl" && c === "O") remoteName = true;
2818
+ if (name === "curl" && c === "J") serverNamed = true;
2819
+ if (name === "wget" && (c === "r" || c === "m" || c === "p")) serverNamed = true;
2820
+ if (valueShort.includes(c)) {
2821
+ const rest = v.slice(j + 1);
2822
+ take(`-${c}`, rest ? { value: rest } : args[++i] ?? null);
2823
+ break;
2824
+ }
2825
+ }
2826
+ continue;
2827
+ }
2828
+ if (/^(?:https?|ftps?):\/\//i.test(v)) urls.push(v);
2829
+ }
2830
+ for (const out2 of outputs) {
2831
+ const spelled = name === "aria2c" && dir2?.value && !out2.value.startsWith("/") ? { ...out2, value: `${dir2.value.replace(/\/+$/, "")}/${out2.value}` } : out2;
2832
+ if (name === "curl" && /#\d/.test(spelled.value))
2833
+ add("write", { value: "", pattern: spelled.value.replace(/#\d+/g, "*"), dot: true }, state);
2834
+ else add("write", spelled, state);
2835
+ }
2836
+ const remote = name === "curl" ? remoteName || serverNamed : !document || serverNamed;
2837
+ if (!remote) return;
2838
+ const into = dir2?.value ? dir2.value.replace(/\/+$/, "") : ".";
2839
+ if (serverNamed || urls.length === 0) {
2840
+ add("write", { value: "", pattern: `${quoteGlob(into)}/*`, dot: true }, state);
2841
+ return;
2842
+ }
2843
+ for (const url of urls) {
2844
+ const file2 = url.replace(/[?#].*$/, "").replace(/^[a-z]+:\/\/[^/]*/i, "").split("/").pop();
2845
+ add("write", { value: `${into}/${file2 || "index.html"}` }, state);
2846
+ }
2847
+ }
2617
2848
  function findActions2(tokens, words, head, state, nested, depth) {
2618
2849
  const starts = findStartPoints(words, head).starts;
2619
2850
  const roots = (starts.length ? starts : ["."]).slice(0, MAX_ALTERNATIVES);
@@ -2685,7 +2916,35 @@ function collectShellFileAccesses(command, context = {}) {
2685
2916
  const words = wordValues(node, state);
2686
2917
  alternatives = alternatives.flatMap((prefix) => (words.length ? words : [null]).map((w) => [...prefix, w])).slice(0, MAX_ALTERNATIVES);
2687
2918
  }
2688
- for (const tokens of alternatives) fromWords(tokens, state, nested, depth);
2919
+ for (const tokens of alternatives) {
2920
+ fromWords(tokens, state, nested, depth);
2921
+ inlineCall(tokens, state, depth);
2922
+ }
2923
+ }
2924
+ function inlineCall(tokens, state, depth) {
2925
+ const head = tokens[0];
2926
+ if (!head || head.dynamic || head.pattern) return;
2927
+ const bodies = functions.get(head.value);
2928
+ if (!bodies || (state.callDepth ?? 0) >= MAX_CALL_DEPTH2) return;
2929
+ const args = tokens.slice(1).map((a) => a && !a.dynamic ? a.value : null);
2930
+ const vars = JSON.stringify([...state.vars].sort(([a], [b]) => a < b ? -1 : 1));
2931
+ const key = `${head.value}\0${JSON.stringify(args)}\0${state.cwd.join("")}\0${vars}`;
2932
+ if (seenCalls.has(key)) return;
2933
+ seenCalls.add(key);
2934
+ for (const body of bodies) {
2935
+ if (callWalks2-- <= 0) return;
2936
+ const local = clone(state);
2937
+ local.callDepth = (state.callDepth ?? 0) + 1;
2938
+ for (const name of POSITIONAL) local.vars.delete(name);
2939
+ if (!rebindsPositionals(body)) {
2940
+ args.slice(0, 9).forEach((v, i) => local.vars.set(String(i + 1), v === null ? null : [v]));
2941
+ const all = args.every((v) => v !== null) && args.length ? unique(args) : null;
2942
+ local.vars.set("@", all);
2943
+ local.vars.set("*", all);
2944
+ local.vars.set("#", [String(args.length)]);
2945
+ }
2946
+ statement(body, local, depth + 1);
2947
+ }
2689
2948
  }
2690
2949
  function readsScriptFromStdin(stmt) {
2691
2950
  const cmd = stmt?.Cmd;
@@ -2722,11 +2981,13 @@ function collectShellFileAccesses(command, context = {}) {
2722
2981
  if (!stmt || depth > MAX_DEPTH) return;
2723
2982
  state.onSuccess = void 0;
2724
2983
  if (stmt.Background) state = clone(state);
2984
+ const headWord = stmt.Cmd && syntax2.NodeType(stmt.Cmd) === "CallExpr" ? base(resolveWordLiteral(stmt.Cmd.Args?.[0])) : "";
2985
+ const sends = SENDING_STDIN.has(headWord);
2725
2986
  for (const redir of stmt.Redirs ?? []) {
2726
2987
  substitutions(redir.Word, state, depth);
2727
2988
  for (const word of wordValues(redir.Word, state)) {
2728
2989
  if (WRITES.has(redir.Op)) add("write", word, state);
2729
- if (READS.has(redir.Op)) add("read", word, state);
2990
+ if (READS.has(redir.Op)) add("read", word, state, sends ? { upload: true } : {});
2730
2991
  if (redir.Op === DUP_OUT && word.value && !/^\d+$/.test(word.value))
2731
2992
  add("write", word, state);
2732
2993
  }
@@ -2835,7 +3096,10 @@ function collectShellFileAccesses(command, context = {}) {
2835
3096
  }
2836
3097
  const parsed = parseShared(command);
2837
3098
  const program = typeof parsed !== "symbol" ? parsed : runnablePrefix(command);
2838
- if (program) statements(program.Stmts ?? [], initial, 0);
3099
+ if (program) {
3100
+ for (const [name, bodies] of functionBodies(program)) functions.set(name, bodies);
3101
+ statements(program.Stmts ?? [], initial, 0);
3102
+ }
2839
3103
  return out;
2840
3104
  }
2841
3105
  function isAgentInstructionPath(absPath, root) {
@@ -2951,6 +3215,33 @@ function hits(access, paths, directories, context) {
2951
3215
  }
2952
3216
  return false;
2953
3217
  }
3218
+ function hitsSystemDir(access, context, dirs = SYSTEM_DIRS) {
3219
+ const home = context.homeDirs?.[0] ?? "~";
3220
+ const candidates = access.candidates ?? [
3221
+ normalizeFilePath(access.path, access.cwd ?? context.cwd, home)
3222
+ ];
3223
+ for (const p of candidates) {
3224
+ if (dirs.some((d) => within(p, d)) && !exempt(p)) return true;
3225
+ if (access.recursive && dirs.some((d) => within(d, p))) return true;
3226
+ }
3227
+ if (!access.glob) return false;
3228
+ const pattern = normalizeFilePath(access.glob, access.cwd ?? context.cwd, home);
3229
+ const magic = pattern.search(/[*?[{]/);
3230
+ const literal = magic < 0 ? pattern : pattern.slice(0, pattern.lastIndexOf("/", magic) + 1);
3231
+ if (literal && exempt(literal.replace(/\/$/, "") || "/")) return false;
3232
+ const options = { dot: access.dotGlob === true, nonegate: true };
3233
+ const components = pattern.split("/");
3234
+ for (const root of dirs) {
3235
+ const count = root.split("/").length;
3236
+ if (components.length > count && (0, import_picomatch.default)(components.slice(0, count).join("/"), options)(root))
3237
+ return true;
3238
+ if (access.recursive && (0, import_picomatch.default)(pattern, options)(root)) return true;
3239
+ }
3240
+ return false;
3241
+ }
3242
+ function uploadDirs(context) {
3243
+ return context.homeDirs?.[0] === "/root" ? UPLOAD_SYSTEM_DIRS.filter((d) => d !== "/root") : UPLOAD_SYSTEM_DIRS;
3244
+ }
2954
3245
  function evaluateProtectedFileAccess(accesses, context, settings) {
2955
3246
  const homes = [.../* @__PURE__ */ new Set([...context.homeDirs ?? ["~"], "/root"])];
2956
3247
  const startup = homes.flatMap(
@@ -2972,6 +3263,14 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
2972
3263
  "files.shell-startup",
2973
3264
  "review-shell-startup-mutation",
2974
3265
  "Changing shell startup files requires approval"
3266
+ ] : mutation && hitsSystemDir(access, context) ? [
3267
+ "files.system-dirs",
3268
+ "review-system-dir-mutation",
3269
+ "Changing files in a system directory requires approval"
3270
+ ] : !mutation && access.upload && hitsSystemDir(access, context, uploadDirs(context)) ? [
3271
+ "files.system-dirs",
3272
+ "review-system-file-upload",
3273
+ "Uploading a system file requires approval"
2975
3274
  ] : !mutation && hits(access, PASSWORD_HASH_FILES, [], context) ? [
2976
3275
  "files.password-hashes",
2977
3276
  "review-password-hash-read",
@@ -3232,16 +3531,21 @@ function looksGenerated(value, minLength, minEntropy, lettersOnlyAllowed = false
3232
3531
  if (EMAIL_VALUE_RE.test(value)) return false;
3233
3532
  if (looksLikeJoinedWords(value) || hasSequentialRun(value)) return false;
3234
3533
  if (PATH_VALUE_RE.test(value) || DOTTED_REFERENCE_RE.test(value)) return false;
3534
+ if (CONCATENATION_RE.test(value)) return false;
3235
3535
  if (rejectedByStructuredValidator(value)) return false;
3236
3536
  return shannonEntropy(value) >= minEntropy;
3237
3537
  }
3238
3538
  function nameHoldsCredential(name) {
3239
3539
  return !FIXTURE_NAME_RE.test(name) && !DERIVATIVE_NAME_RE.test(name);
3240
3540
  }
3541
+ function isOtpDocSecret(value) {
3542
+ return OTP_DOC_SECRETS.has(value.replace(/=+$/, "").toUpperCase());
3543
+ }
3241
3544
  function validateQueryCredential(raw) {
3242
3545
  const value = capturedValue(raw, QUERY_CREDENTIAL_VALUE_RE);
3243
3546
  if (value === null) return false;
3244
- if (OTP_SECRET_RE.test(value)) return looksGenerated(value, 16, 3, true);
3547
+ if (OTP_SECRET_RE.test(value))
3548
+ return !isOtpDocSecret(value) && looksGenerated(value, 16, 3, true);
3245
3549
  if (!/[0-9]/.test(value) || !/[A-Za-z]/.test(value)) return false;
3246
3550
  return looksGenerated(value, 12, 3);
3247
3551
  }
@@ -3255,13 +3559,40 @@ function keywordEndsAPart(name) {
3255
3559
  }
3256
3560
  function validateAssignedSecret(raw, before = "") {
3257
3561
  const value = capturedValue(raw, ASSIGNED_SECRET_VALUE_RE);
3258
- const name = capturedValue(raw, ASSIGNED_SECRET_NAME_RE);
3562
+ const name = capturedValue(raw, ASSIGNED_SECRET_NAME_RE)?.replace(/^_+/, "") ?? null;
3259
3563
  if (value === null || name === null) return false;
3260
- if (!keywordEndsAPart(name) || ARN_CONTEXT_RE.test(before)) return false;
3261
- const prefix = before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "";
3564
+ if (!keywordEndsAPart(name)) return false;
3565
+ if (ARN_CONTEXT_RE.test(before)) return false;
3566
+ const prefix = (before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "") + (raw.startsWith("_") ? "_" : "");
3262
3567
  if (!nameHoldsCredential(prefix + name)) return false;
3263
3568
  return looksGenerated(value, 12, 3.5);
3264
3569
  }
3570
+ function validateBasicCredential(raw) {
3571
+ const b64 = capturedValue(raw, BASIC_VALUE_RE);
3572
+ if (b64 === null) return false;
3573
+ const bare = b64.replace(/=+$/, "");
3574
+ if (bare.length % 4 === 1) return false;
3575
+ const bytes = Buffer.from(b64, "base64");
3576
+ if (bytes.toString("base64").replace(/=+$/, "") !== bare) return false;
3577
+ const text = bytes.toString("utf8");
3578
+ if (!Buffer.from(text, "utf8").equals(bytes)) return false;
3579
+ for (let i = 0; i < text.length; i++) {
3580
+ const c = text.charCodeAt(i);
3581
+ if (c < 32 || c === 127) return false;
3582
+ }
3583
+ const pair = BASIC_PAIR_RE.exec(text);
3584
+ if (!pair) return false;
3585
+ const password = pair[2];
3586
+ if (!/[0-9]/.test(password) || !/[A-Za-z]/.test(password)) return false;
3587
+ const lower2 = password.toLowerCase();
3588
+ if (DLP_STOPWORDS.some((sw) => lower2.includes(sw)) || hasSequentialRun(password)) return false;
3589
+ return shannonEntropy(password) >= 3;
3590
+ }
3591
+ function validateOtpSecret(raw) {
3592
+ const secret = capturedValue(raw, OTP_CAPTURE_RE);
3593
+ if (secret === null || isOtpDocSecret(secret)) return false;
3594
+ return looksGenerated(secret.replace(/=+$/, ""), 16, 3, true);
3595
+ }
3265
3596
  function matchSensitivePath2(resolvedPath, originalPath, options = {}) {
3266
3597
  const normalised = resolvedPath.replace(/\\/g, "/");
3267
3598
  if (!options.systemAccessHandled && isLegacySystemPath(normalised))
@@ -3532,6 +3863,156 @@ function parsePySpec(raw) {
3532
3863
  const exact = PY_EXACT_RE.exec(m[2].trim());
3533
3864
  return { name, version: exact ? exact[1] : void 0 };
3534
3865
  }
3866
+ function exactVersion(v) {
3867
+ if (!v) return void 0;
3868
+ const m = EXACT_VERSION_RE.exec(v.trim());
3869
+ return m ? m[1] : void 0;
3870
+ }
3871
+ function push(out, ecosystem, manager, name, version, raw) {
3872
+ out.push({ ecosystem, manager, name, ...version ? { version } : {}, raw });
3873
+ }
3874
+ function gemInstall(words, from, out) {
3875
+ const dash = words.indexOf("--", from);
3876
+ const own = dash < 0 ? words : words.slice(0, dash);
3877
+ const pinned = exactVersion(flagValues(own, from, ["-v", "--version"])[0]);
3878
+ for (const raw of operands(own, from, GEM_VALUE_FLAGS)) {
3879
+ if (NON_REGISTRY_RE.test(raw) || /\.gem$/i.test(raw)) continue;
3880
+ const colon = raw.indexOf(":");
3881
+ const name = colon < 0 ? raw : raw.slice(0, colon);
3882
+ if (!GEM_NAME_RE.test(name)) continue;
3883
+ push(
3884
+ out,
3885
+ "RubyGems",
3886
+ "gem",
3887
+ name,
3888
+ colon < 0 ? pinned : exactVersion(raw.slice(colon + 1)),
3889
+ raw
3890
+ );
3891
+ }
3892
+ }
3893
+ function cargoCrates(words, from, manager, out) {
3894
+ const flags = manager === "cargo add" ? CARGO_ADD_VALUE_FLAGS : CARGO_INSTALL_VALUE_FLAGS;
3895
+ if (flagValues(words, from, ["--git", "--path", "--registry", "--index"]).length > 0) return;
3896
+ if (words.some((w) => w === "--git" || w === "--path")) return;
3897
+ const flagVersion = manager === "cargo install" ? exactVersion(flagValues(words, from, ["--version", "--vers"])[0]) : void 0;
3898
+ for (const raw of operands(words, from, flags)) {
3899
+ const at = raw.indexOf("@");
3900
+ const name = at < 0 ? raw : raw.slice(0, at);
3901
+ if (!CRATE_NAME_RE.test(name)) continue;
3902
+ push(
3903
+ out,
3904
+ "crates.io",
3905
+ manager,
3906
+ name,
3907
+ at < 0 ? flagVersion : exactVersion(raw.slice(at + 1)),
3908
+ raw
3909
+ );
3910
+ }
3911
+ }
3912
+ function goModules(words, from, manager, out) {
3913
+ let specs = operands(words, from, GO_VALUE_FLAGS);
3914
+ if (manager === "go run") specs = specs.slice(0, 1).filter((s) => s.includes("@"));
3915
+ for (const raw of specs) {
3916
+ const at = raw.indexOf("@");
3917
+ const pathPart = (at < 0 ? raw : raw.slice(0, at)).replace(/\/\.\.\.$/, "");
3918
+ const ver = at < 0 ? void 0 : raw.slice(at + 1);
3919
+ if (ver === "none") continue;
3920
+ if (!GO_PATH_RE.test(pathPart) || /\.go$/i.test(pathPart)) continue;
3921
+ const exact = ver && /^v\d/.test(ver) ? exactVersion(ver) : void 0;
3922
+ push(out, "Go", manager, pathPart, exact, raw);
3923
+ }
3924
+ }
3925
+ function dotnetAdd(words, from, out) {
3926
+ let k = -1;
3927
+ for (let i = from; i < Math.min(words.length, from + 2); i++) {
3928
+ if (lower(words[i]) === "package") {
3929
+ k = i;
3930
+ break;
3931
+ }
3932
+ }
3933
+ if (k < 0) return;
3934
+ const name = operands(words, k + 1, DOTNET_ADD_VALUE_FLAGS)[0];
3935
+ if (!name || !NUGET_NAME_RE.test(name)) return;
3936
+ const version = exactVersion(flagValues(words, k + 1, ["-v", "--version"])[0]);
3937
+ push(out, "NuGet", "dotnet add package", name, version, name);
3938
+ }
3939
+ function dotnetTool(words, from, out) {
3940
+ const name = operands(words, from, DOTNET_TOOL_VALUE_FLAGS)[0];
3941
+ if (!name || !NUGET_NAME_RE.test(name)) return;
3942
+ const version = exactVersion(flagValues(words, from, ["--version"])[0]);
3943
+ push(out, "NuGet", "dotnet tool", name, version, name);
3944
+ }
3945
+ function nugetInstall(words, from, out) {
3946
+ const lowered = words.map((w) => w !== null && w.startsWith("-") ? w.toLowerCase() : w);
3947
+ const name = operands(lowered, from, NUGET_VALUE_FLAGS)[0];
3948
+ if (!name || !NUGET_NAME_RE.test(name) || /\.config$/i.test(name)) return;
3949
+ const version = exactVersion(flagValues(lowered, from, ["-version"])[0]);
3950
+ push(out, "NuGet", "nuget install", name, version, name);
3951
+ }
3952
+ function mavenGet(words, from, out) {
3953
+ const goal = words.slice(from).some(
3954
+ (w) => w !== null && /(?:^|:)dependency:get$|^org\.apache\.maven\.plugins:maven-dependency-plugin:(?:[\w.-]+:)?get$/.test(
3955
+ w
3956
+ )
3957
+ );
3958
+ if (!goal) return;
3959
+ const prop = (key) => {
3960
+ for (let i = from; i < words.length; i++) {
3961
+ const w = words[i];
3962
+ if (w === null) continue;
3963
+ if (w.startsWith(`-D${key}=`)) return w.slice(key.length + 3);
3964
+ if (w === "-D" && words[i + 1]?.startsWith(`${key}=`))
3965
+ return words[i + 1].slice(key.length + 1);
3966
+ }
3967
+ return void 0;
3968
+ };
3969
+ const artifact = prop("artifact");
3970
+ let coord;
3971
+ let ver;
3972
+ if (artifact) {
3973
+ const parts = artifact.split(":");
3974
+ if (parts.length < 2) return;
3975
+ coord = `${parts[0]}:${parts[1]}`;
3976
+ ver = parts[2];
3977
+ } else {
3978
+ const g = prop("groupId");
3979
+ const a = prop("artifactId");
3980
+ if (!g || !a) return;
3981
+ coord = `${g}:${a}`;
3982
+ ver = prop("version");
3983
+ }
3984
+ if (!MAVEN_COORD_RE.test(coord)) return;
3985
+ const exact = ver && !/^(?:LATEST|RELEASE)$/i.test(ver) ? exactVersion(ver) : void 0;
3986
+ push(out, "Maven", "mvn dependency:get", coord, exact, artifact ?? coord);
3987
+ }
3988
+ function composerPackages(words, from, manager, out) {
3989
+ const ops = operands(words, from, COMPOSER_VALUE_FLAGS);
3990
+ const specs = manager === "composer create-project" ? ops.slice(0, 1) : ops;
3991
+ for (const raw of specs) {
3992
+ const sep = raw.search(/[:=]/);
3993
+ const name = sep < 0 ? raw : raw.slice(0, sep);
3994
+ if (!COMPOSER_NAME_RE.test(name)) continue;
3995
+ let ver = sep < 0 ? void 0 : raw.slice(sep + 1);
3996
+ if (manager === "composer create-project" && ver === void 0) ver = ops[2] ?? void 0;
3997
+ push(out, "Packagist", manager, name.toLowerCase(), exactVersion(ver), raw);
3998
+ }
3999
+ }
4000
+ function editorExtensions(words, from, out) {
4001
+ for (const raw of flagValues(words, from, ["--install-extension"])) {
4002
+ if (/\.vsix$/i.test(raw) || NON_REGISTRY_RE.test(raw)) continue;
4003
+ const at = raw.lastIndexOf("@");
4004
+ const name = at <= 0 ? raw : raw.slice(0, at);
4005
+ if (!EXTENSION_ID_RE.test(name)) continue;
4006
+ push(
4007
+ out,
4008
+ "VSCode",
4009
+ "code --install-extension",
4010
+ name,
4011
+ at <= 0 ? void 0 : exactVersion(raw.slice(at + 1)),
4012
+ raw
4013
+ );
4014
+ }
4015
+ }
3535
4016
  function basename(w) {
3536
4017
  return (w ?? "").toLowerCase().split("/").pop() ?? "";
3537
4018
  }
@@ -3763,7 +4244,63 @@ function fromCall(words, out, depth) {
3763
4244
  pushPy(out, "pipenv", operands(words, v + 1, PIPENV_VALUE_FLAGS));
3764
4245
  return;
3765
4246
  }
4247
+ case "gem": {
4248
+ const v = verbIndex(words, start + 1, GEM_VALUE_FLAGS);
4249
+ if (v >= 0 && ["install", "i"].includes(lower(words[v]))) gemInstall(words, v + 1, out);
4250
+ return;
4251
+ }
4252
+ case "bundle": {
4253
+ const v = verbIndex(words, start + 1, BUNDLE_VALUE_FLAGS);
4254
+ if (v < 0 || lower(words[v]) !== "add") return;
4255
+ if (flagValues(words, v + 1, ["--git", "--github", "--path"]).length > 0) return;
4256
+ const version = exactVersion(flagValues(words, v + 1, ["-v", "--version"])[0]);
4257
+ for (const raw of operands(words, v + 1, BUNDLE_VALUE_FLAGS))
4258
+ if (GEM_NAME_RE.test(raw)) push(out, "RubyGems", "bundle add", raw, version, raw);
4259
+ return;
4260
+ }
4261
+ case "cargo": {
4262
+ const from = words[start + 1]?.startsWith("+") ? start + 2 : start + 1;
4263
+ const v = verbIndex(words, from, CARGO_ADD_VALUE_FLAGS);
4264
+ const verb = lower(words[v]);
4265
+ if (verb === "add") cargoCrates(words, v + 1, "cargo add", out);
4266
+ else if (verb === "install") cargoCrates(words, v + 1, "cargo install", out);
4267
+ return;
4268
+ }
4269
+ case "go": {
4270
+ const v = verbIndex(words, start + 1, GO_VALUE_FLAGS);
4271
+ const verb = lower(words[v]);
4272
+ if (verb === "get" || verb === "install" || verb === "run")
4273
+ goModules(words, v + 1, `go ${verb}`, out);
4274
+ return;
4275
+ }
4276
+ case "dotnet": {
4277
+ const v = verbIndex(words, start + 1, DOTNET_ADD_VALUE_FLAGS);
4278
+ const verb = lower(words[v]);
4279
+ if (verb === "add") dotnetAdd(words, v + 1, out);
4280
+ else if (verb === "tool" && ["install", "update"].includes(lower(words[v + 1])))
4281
+ dotnetTool(words, v + 2, out);
4282
+ return;
4283
+ }
4284
+ case "nuget": {
4285
+ if (lower(words[start + 1]) === "install") nugetInstall(words, start + 2, out);
4286
+ return;
4287
+ }
4288
+ case "mvn":
4289
+ case "mvnw":
4290
+ mavenGet(words, start + 1, out);
4291
+ return;
4292
+ case "composer": {
4293
+ let v = verbIndex(words, start + 1, COMPOSER_VALUE_FLAGS);
4294
+ if (v >= 0 && lower(words[v]) === "global") v = verbIndex(words, v + 1, COMPOSER_VALUE_FLAGS);
4295
+ const verb = lower(words[v]);
4296
+ if (verb === "require" || verb === "req" || verb === "r")
4297
+ composerPackages(words, v + 1, "composer require", out);
4298
+ else if (verb === "create-project")
4299
+ composerPackages(words, v + 1, "composer create-project", out);
4300
+ return;
4301
+ }
3766
4302
  default:
4303
+ if (EDITOR_HEADS.includes(family)) editorExtensions(words, start + 1, out);
3767
4304
  return;
3768
4305
  }
3769
4306
  }
@@ -5230,12 +5767,13 @@ function gitRepoPath(url) {
5230
5767
  function isDestructiveToolName(bareTool) {
5231
5768
  return DESTRUCTIVE_VERB_RE.test(bareTool);
5232
5769
  }
5233
- function* validCardRuns(text, base2) {
5770
+ function* validCardRuns(text, base2, exemptFractions = true) {
5234
5771
  const re = new RegExp(base2.source, "g");
5235
5772
  let m;
5236
5773
  while ((m = re.exec(text)) !== null) {
5237
5774
  const digits = m[0].replace(/\D/g, "");
5238
- if (!isDecimalFraction(text, m.index, digits.length === m[0].length) && validateLuhn(digits)) {
5775
+ const fraction = exemptFractions && isDecimalFraction(text, m.index, digits.length === m[0].length);
5776
+ if (!fraction && validateLuhn(digits)) {
5239
5777
  yield { index: m.index, length: m[0].length, digits };
5240
5778
  } else {
5241
5779
  re.lastIndex = m.index + 1;
@@ -5248,12 +5786,12 @@ function hasValidCard(text) {
5248
5786
  }
5249
5787
  return false;
5250
5788
  }
5251
- function replaceValidCards(text, replace) {
5789
+ function replaceValidCards(text, replace, { exemptFractions = true } = {}) {
5252
5790
  let out = text;
5253
5791
  for (const base2 of [PII_CC16_RE, PII_CC15_RE]) {
5254
5792
  let res = "";
5255
5793
  let last = 0;
5256
- for (const run of validCardRuns(out, base2)) {
5794
+ for (const run of validCardRuns(out, base2, exemptFractions)) {
5257
5795
  res += out.slice(last, run.index) + replace(run.digits);
5258
5796
  last = run.index + run.length;
5259
5797
  }
@@ -5267,11 +5805,11 @@ function isDecimalFraction(text, index, unseparated) {
5267
5805
  const a = text[index - 2];
5268
5806
  if (sep === ".") {
5269
5807
  if (a !== void 0 && a >= "0" && a <= "9") return true;
5270
- return a === void 0 || NUMBER_LEAD_RE.test(a);
5808
+ return a === void 0 || (a === "-" || a === "+") && index === 2;
5271
5809
  }
5272
5810
  if (sep === "," && a === "0") {
5273
5811
  const b = text[index - 3];
5274
- return b === void 0 || NUMBER_LEAD_RE.test(b);
5812
+ return b === void 0 || b === ";";
5275
5813
  }
5276
5814
  return false;
5277
5815
  }
@@ -5407,6 +5945,9 @@ function normalizeTrustedEntry(raw) {
5407
5945
  return full === null ? null : trustedEntryOf(full);
5408
5946
  }
5409
5947
  function maskPersonalData(text) {
5948
+ return maskText(text, false);
5949
+ }
5950
+ function maskText(text, exemptFractions) {
5410
5951
  if (!text) return text;
5411
5952
  try {
5412
5953
  let out = text;
@@ -5414,7 +5955,9 @@ function maskPersonalData(text) {
5414
5955
  const v = m.replace(/[ -]/g, "").toUpperCase();
5415
5956
  return validateIban(v) ? `${v.slice(0, 4)} **** **** ${v.slice(-4)}` : m;
5416
5957
  });
5417
- out = replaceValidCards(out, (digits) => `**** **** **** ${digits.slice(-4)}`);
5958
+ out = replaceValidCards(out, (digits) => `**** **** **** ${digits.slice(-4)}`, {
5959
+ exemptFractions
5960
+ });
5418
5961
  out = out.replace(
5419
5962
  ACCOUNT_VALUE_RE,
5420
5963
  (m, v) => IBAN_LENGTH[v.slice(0, 2).toUpperCase()] === v.length ? `"****${v.slice(-4)}"` : m
@@ -5437,7 +5980,7 @@ function maskLeaf(value, key, depth) {
5437
5980
  if (depth > MAX_MASK_DEPTH) return "[REDACTED]";
5438
5981
  if (typeof value === "string") {
5439
5982
  try {
5440
- const json = `{${JSON.stringify(key)}:${JSON.stringify(value)}}`;
5983
+ const json = `{${JSON.stringify(key)}:${JSON.stringify(maskPersonalData(value))}}`;
5441
5984
  return Object.values(JSON.parse(maskPersonalData(json)))[0];
5442
5985
  } catch {
5443
5986
  return "[REDACTED]";
@@ -5445,13 +5988,20 @@ function maskLeaf(value, key, depth) {
5445
5988
  }
5446
5989
  if (typeof value === "number") {
5447
5990
  const text = String(value);
5448
- const masked = maskPersonalData(text);
5991
+ const masked = maskText(text, true);
5449
5992
  return masked === text ? value : masked;
5450
5993
  }
5451
5994
  if (Array.isArray(value)) return value.map((v) => maskLeaf(v, key, depth + 1));
5452
5995
  if (value && typeof value === "object") {
5996
+ const used = /* @__PURE__ */ new Set();
5453
5997
  return Object.fromEntries(
5454
- Object.entries(value).map(([k, v]) => [maskPersonalData(k), maskLeaf(v, k, depth + 1)])
5998
+ Object.entries(value).map(([k, v]) => {
5999
+ const base2 = maskPersonalData(k);
6000
+ let name = base2;
6001
+ for (let n = 2; used.has(name); n++) name = `${base2}#${n}`;
6002
+ used.add(name);
6003
+ return [name, maskLeaf(v, k, depth + 1)];
6004
+ })
5455
6005
  );
5456
6006
  }
5457
6007
  return value;
@@ -5933,7 +6483,7 @@ function matchCanaryArgs(args, values) {
5933
6483
  return null;
5934
6484
  }
5935
6485
  }
5936
- 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, 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, 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, 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, LOOP_MAX_RECORDS, 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;
6486
+ 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, 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, 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, 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, LOOP_MAX_RECORDS, 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;
5937
6487
  var init_dist = __esm({
5938
6488
  "packages/policy-engine/dist/index.mjs"() {
5939
6489
  "use strict";
@@ -6747,6 +7297,11 @@ var init_dist = __esm({
6747
7297
  example: "cat /etc/shadow",
6748
7298
  advice: "Keep Review, or choose Block when agents must never read system password hashes."
6749
7299
  },
7300
+ "files.system-dirs": {
7301
+ 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.",
7302
+ example: "wget -O /usr/bin/tool https://example.com/tool",
7303
+ advice: "Keep Review on a workstation. A container image build that installs into /usr can set it to Log."
7304
+ },
6750
7305
  // ── Commands ──────────────────────────────────────────────────────────────
6751
7306
  "commands.inline-exec": {
6752
7307
  plain: "The agent runs a small program written inside the command itself.",
@@ -6943,7 +7498,7 @@ var init_dist = __esm({
6943
7498
  advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
6944
7499
  },
6945
7500
  "loading.malicious-package": {
6946
- plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago.",
7501
+ 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.",
6947
7502
  example: "npm install of a package flagged as malware",
6948
7503
  advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
6949
7504
  }
@@ -6981,6 +7536,15 @@ var init_dist = __esm({
6981
7536
  defaultValue: "review",
6982
7537
  legacy: ["review-password-hash-read"]
6983
7538
  },
7539
+ {
7540
+ id: "files.system-dirs",
7541
+ group: "files",
7542
+ title: "System directories",
7543
+ catches: "Writes, new files and removals under /usr, /etc, /boot, /bin, /sbin, /lib and /var/log, and uploads of system files",
7544
+ values: ALL,
7545
+ defaultValue: "review",
7546
+ legacy: ["review-system-dir-mutation", "review-system-file-upload"]
7547
+ },
6984
7548
  // ── Commands ────────────────────────────────────────────────────────────
6985
7549
  {
6986
7550
  id: "commands.inline-exec",
@@ -8278,6 +8842,10 @@ var init_dist = __esm({
8278
8842
  AND_OP = probeBinOp("a && b");
8279
8843
  OR_OP = probeBinOp("a || b");
8280
8844
  AND_OR_OPS = new Set([AND_OP, OR_OP].filter((o) => o !== null));
8845
+ MAX_CALL_DEPTH = 4;
8846
+ MAX_CALL_WALKS = 256;
8847
+ callDepth = 0;
8848
+ callWalks = 0;
8281
8849
  FS_OP_CACHE_MAX = 5e3;
8282
8850
  fsOpCache = /* @__PURE__ */ new Map();
8283
8851
  stripDotSlash = (p) => p.replace(/^\.\//, "");
@@ -8330,10 +8898,79 @@ var init_dist = __esm({
8330
8898
  MAX_ARGV_CHARACTERS = 262144;
8331
8899
  ({ syntax: syntax2 } = import_mvdan_sh2.default);
8332
8900
  MAX_ALTERNATIVES = 64;
8901
+ MAX_CALL_DEPTH2 = 4;
8902
+ MAX_CALL_WALKS2 = 256;
8903
+ POSITIONAL = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*", "#"];
8333
8904
  MAX_DEPTH = 64;
8334
8905
  MAX_PAYLOAD_DEPTH = 24;
8335
8906
  unique = (a) => [...new Set(a)].slice(0, MAX_ALTERNATIVES);
8336
- clone = (s) => ({ cwd: [...s.cwd], vars: new Map(s.vars) });
8907
+ clone = (s) => ({
8908
+ cwd: [...s.cwd],
8909
+ vars: new Map(s.vars),
8910
+ ...s.callDepth ? { callDepth: s.callDepth } : {}
8911
+ });
8912
+ SENDING_STDIN = /* @__PURE__ */ new Set(["nc", "ncat", "netcat", "socat", "telnet", "curl", "wget"]);
8913
+ DOWNLOAD_SHORT_VALUE = {
8914
+ curl: "odDeEFHKmPrtTuUwxXyYzAbcCQ",
8915
+ wget: "OoaPiBetTwQlARDXIUY",
8916
+ aria2c: "odisx"
8917
+ };
8918
+ DOWNLOAD_LONG_VALUE = {
8919
+ curl: [
8920
+ "--output",
8921
+ "--output-dir",
8922
+ "--dump-header",
8923
+ "--cookie-jar",
8924
+ "--data",
8925
+ "--data-binary",
8926
+ "--data-raw",
8927
+ "--data-ascii",
8928
+ "--data-urlencode",
8929
+ "--form",
8930
+ "--header",
8931
+ "--request",
8932
+ "--user",
8933
+ "--user-agent",
8934
+ "--referer",
8935
+ "--cookie",
8936
+ "--config",
8937
+ "--json",
8938
+ "--upload-file",
8939
+ "--max-time",
8940
+ "--proxy",
8941
+ "--write-out",
8942
+ "--url"
8943
+ ],
8944
+ wget: [
8945
+ "--output-document",
8946
+ "--output-file",
8947
+ "--append-output",
8948
+ "--directory-prefix",
8949
+ "--input-file",
8950
+ "--post-data",
8951
+ "--post-file",
8952
+ "--body-data",
8953
+ "--body-file",
8954
+ "--method",
8955
+ "--header",
8956
+ "--user-agent",
8957
+ "--user",
8958
+ "--password",
8959
+ "--tries",
8960
+ "--timeout"
8961
+ ],
8962
+ aria2c: ["--out", "--dir", "--input-file"]
8963
+ };
8964
+ DOWNLOAD_OUTPUT_FLAGS = {
8965
+ curl: ["-o", "--output", "-D", "--dump-header", "-c", "--cookie-jar"],
8966
+ wget: ["-O", "--output-document", "-o", "--output-file", "-a", "--append-output"],
8967
+ aria2c: ["-o", "--out"]
8968
+ };
8969
+ DOWNLOAD_DIR_FLAGS = {
8970
+ curl: ["--output-dir"],
8971
+ wget: ["-P", "--directory-prefix"],
8972
+ aria2c: ["-d", "--dir"]
8973
+ };
8337
8974
  base = (s) => (s ?? "").replace(/\/+$/, "").split("/").pop() ?? "";
8338
8975
  quoteGlob = (s) => s.replace(/[\\*?\[\]{}()!+@]/g, "\\$&");
8339
8976
  asWord = (p) => ({ value: p.value, ...p.glob ? { pattern: p.pattern } : {} });
@@ -8358,6 +8995,18 @@ var init_dist = __esm({
8358
8995
  SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
8359
8996
  PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
8360
8997
  STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
8998
+ SYSTEM_DIRS = [
8999
+ "/usr",
9000
+ "/etc",
9001
+ "/boot",
9002
+ "/bin",
9003
+ "/sbin",
9004
+ "/lib",
9005
+ "/lib64",
9006
+ "/var/log"
9007
+ ];
9008
+ SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
9009
+ UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
8361
9010
  AGENT_INSTRUCTION_FILES = [
8362
9011
  ".gitlab-ci.yml",
8363
9012
  "CLAUDE.md",
@@ -8375,6 +9024,7 @@ var init_dist = __esm({
8375
9024
  ".husky"
8376
9025
  ];
8377
9026
  LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
9027
+ exempt = (p) => SYSTEM_DIRS_EXEMPT.some((d) => within(p, d));
8378
9028
  RANK = { allow: 0, review: 1, block: 2 };
8379
9029
  BIP39_ENGLISH = `abandon
8380
9030
  ability
@@ -10660,15 +11310,20 @@ zoo`.split("\n");
10660
11310
  IDENTIFIER_VALUE_RE = /^[A-Za-z_.-]+$/;
10661
11311
  PATH_VALUE_RE = /^(?:\/|\.{1,2}\/|~\/)/;
10662
11312
  DOTTED_REFERENCE_RE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/;
11313
+ CONCATENATION_RE = /^\+[A-Za-z_$][\w$]*\./;
10663
11314
  EMAIL_VALUE_RE = /^[^@\s]+@[^@\s]+\.[A-Za-z]{2,}$/;
10664
11315
  FIXTURE_NAME_RE = /(?:fake|dummy|example|sample|mock|stub|placeholder|test)/i;
10665
- 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;
11316
+ 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;
10666
11317
  QUERY_CREDENTIAL_VALUE_RE = /=([A-Za-z0-9%+/_.~-]{8,512}={0,2})$/;
10667
11318
  OTP_SECRET_RE = /^[A-Z2-7]{16,}=*$/i;
10668
- ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
11319
+ OTP_DOC_SECRETS = /* @__PURE__ */ new Set(["JBSWY3DPEHPK3PXP", "GEZDGNBVGY3TQOJQ"]);
11320
+ ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
10669
11321
  ASSIGNED_SECRET_NAME_RE = /^([A-Za-z0-9_]+)/;
10670
- NAME_KEYWORD_RE = /^(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret)/i;
11322
+ 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;
10671
11323
  ARN_CONTEXT_RE = /\barn:[A-Za-z0-9-]*:[^\s"'`]*$/i;
11324
+ BASIC_VALUE_RE = /([A-Za-z0-9+/]{8,512}={0,2})$/;
11325
+ BASIC_PAIR_RE = /^([^:\s]{1,128}):(\S{8,256})$/;
11326
+ OTP_CAPTURE_RE = /["']([A-Z2-7]{16,128}=*)["']$/i;
10672
11327
  DLP_PATTERNS = [
10673
11328
  // ── AWS ───────────────────────────────────────────────────────────────────
10674
11329
  {
@@ -11006,6 +11661,22 @@ zoo`.split("\n");
11006
11661
  contextBoost: true,
11007
11662
  minEntropy: 3
11008
11663
  },
11664
+ // ── HTTP Basic credential ─────────────────────────────────────────────────
11665
+ // `Authorization: Basic <base64 of user:password>`, or the bare scheme in a
11666
+ // config list. The validator decodes the value and accepts only a
11667
+ // `user:password` whose password looks generated (letters and digits, 8+
11668
+ // characters, the query-credential rule), so prose ("Basic authentication")
11669
+ // and documentation pairs (`admin:changeme`, `user:pass1234`) stay allowed.
11670
+ // review: the same weight as a bare Bearer token.
11671
+ {
11672
+ name: "HTTP Basic Credential",
11673
+ regex: /\bBasic[ \t]{1,8}([A-Za-z0-9+/]{8,512}={0,2})/i,
11674
+ severity: "review",
11675
+ keywords: ["basic"],
11676
+ validate: validateBasicCredential,
11677
+ redactGroup: 1,
11678
+ argumentsOnly: true
11679
+ },
11009
11680
  // ── Resend ────────────────────────────────────────────────────────────────
11010
11681
  {
11011
11682
  name: "Resend API Key",
@@ -11147,6 +11818,19 @@ zoo`.split("\n");
11147
11818
  validate: validateQueryCredential,
11148
11819
  argumentsOnly: true
11149
11820
  },
11821
+ // ── OTP secret in a constructor ───────────────────────────────────────────
11822
+ // `ROTP::TOTP.new('<base32>')`, `pyotp.TOTP("<base32>")`, `new HOTP('...')`:
11823
+ // the shared secret of a one-time-password generator written as a literal.
11824
+ // The otpauth URI form is `Credential in URL Query`. review.
11825
+ {
11826
+ name: "OTP Secret",
11827
+ regex: /\b(?:TOTP|HOTP|OTP)(?:\.new|\.create)?[ \t]{0,8}\(?[ \t]{0,8}["']([A-Z2-7]{16,128}=*)["']/i,
11828
+ severity: "review",
11829
+ keywords: ["otp"],
11830
+ validate: validateOtpSecret,
11831
+ redactGroup: 1,
11832
+ argumentsOnly: true
11833
+ },
11150
11834
  // ── Secret assigned to a telling name ─────────────────────────────────────
11151
11835
  // `password: ...`, `JWT_SECRET=...`, `api_key = "..."`. The name alone is
11152
11836
  // not a finding: most such lines hold a reference (`process.env.X`), a
@@ -11154,12 +11838,14 @@ zoo`.split("\n");
11154
11838
  // generated: 12+ characters, not an identifier, Shannon entropy of 3.5
11155
11839
  // bits per character or more. review, never block (node9Firewall#348,
11156
11840
  // item 4). Tuned on CredData's public split; its look-alike lines are the
11157
- // negative control.
11841
+ // negative control. A quote may be escaped (`\"access_token\":\"...\"`):
11842
+ // JSON written inside a string literal. The two optional quotes after the
11843
+ // name are written out, not as a counted group, to keep star height 1.
11158
11844
  {
11159
11845
  name: "Assigned Secret",
11160
- 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,
11846
+ 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,
11161
11847
  severity: "review",
11162
- keywords: ["password", "passphrase", "passwd", "pwd", "secret", "token", "key"],
11848
+ keywords: ["pass", "pwd", "secret", "token", "key", "ticket"],
11163
11849
  validate: validateAssignedSecret,
11164
11850
  argumentsOnly: true
11165
11851
  }
@@ -11420,7 +12106,169 @@ zoo`.split("\n");
11420
12106
  ]);
11421
12107
  PY_SPEC_RE = /^([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)(?:\[[^\]]*\])?\s*(.*)$/;
11422
12108
  PY_EXACT_RE = /^===?\s*v?(\d+(?:\.\d+)*(?:[a-zA-Z0-9.+!-]*))$/;
12109
+ EXACT_VERSION_RE = /^=?v?(\d+(?:\.\d+)*(?:[-+][0-9A-Za-z][0-9A-Za-z.+-]*)?)$/;
12110
+ GEM_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
12111
+ GEM_VALUE_FLAGS = /* @__PURE__ */ new Set([
12112
+ "-v",
12113
+ "--version",
12114
+ "-i",
12115
+ "--install-dir",
12116
+ "-n",
12117
+ "--bindir",
12118
+ "--document",
12119
+ "--build-root",
12120
+ "-P",
12121
+ "--trust-policy",
12122
+ "-s",
12123
+ "--source",
12124
+ "-p",
12125
+ "--http-proxy",
12126
+ "--platform",
12127
+ "-g",
12128
+ "--file",
12129
+ "--without"
12130
+ ]);
12131
+ BUNDLE_VALUE_FLAGS = /* @__PURE__ */ new Set([
12132
+ "-v",
12133
+ "--version",
12134
+ "-g",
12135
+ "--group",
12136
+ "-s",
12137
+ "--source",
12138
+ "-r",
12139
+ "--require",
12140
+ "--path",
12141
+ "--git",
12142
+ "--github",
12143
+ "--branch",
12144
+ "--ref",
12145
+ "--glob"
12146
+ ]);
12147
+ CRATE_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_-]*$/u;
12148
+ CARGO_ADD_VALUE_FLAGS = /* @__PURE__ */ new Set([
12149
+ "-F",
12150
+ "--features",
12151
+ "--rename",
12152
+ "--registry",
12153
+ "--manifest-path",
12154
+ "-p",
12155
+ "--package",
12156
+ "--git",
12157
+ "--branch",
12158
+ "--tag",
12159
+ "--rev",
12160
+ "--path",
12161
+ "--base",
12162
+ "--target",
12163
+ "--color",
12164
+ "--config",
12165
+ "-Z",
12166
+ "--lockfile-path"
12167
+ ]);
12168
+ CARGO_INSTALL_VALUE_FLAGS = /* @__PURE__ */ new Set([
12169
+ "--version",
12170
+ "--vers",
12171
+ "--git",
12172
+ "--branch",
12173
+ "--tag",
12174
+ "--rev",
12175
+ "--path",
12176
+ "--root",
12177
+ "--index",
12178
+ "--registry",
12179
+ "-F",
12180
+ "--features",
12181
+ "--profile",
12182
+ "--target",
12183
+ "--target-dir",
12184
+ "-j",
12185
+ "--jobs",
12186
+ "--bin",
12187
+ "--example",
12188
+ "--color",
12189
+ "--config",
12190
+ "-Z"
12191
+ ]);
12192
+ GO_VALUE_FLAGS = /* @__PURE__ */ new Set([
12193
+ "-C",
12194
+ "-tags",
12195
+ "-ldflags",
12196
+ "-gcflags",
12197
+ "-asmflags",
12198
+ "-mod",
12199
+ "-modfile",
12200
+ "-o",
12201
+ "-p",
12202
+ "-pkgdir",
12203
+ "-toolexec",
12204
+ "-exec",
12205
+ "-overlay",
12206
+ "-pgo",
12207
+ "-buildmode",
12208
+ "-compiler",
12209
+ "-gccgoflags",
12210
+ "-installsuffix",
12211
+ "-covermode",
12212
+ "-coverpkg"
12213
+ ]);
12214
+ GO_PATH_RE = /^[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+(?:\/[\p{L}\p{N}_.~+-]+)*$/u;
12215
+ NUGET_NAME_RE = /^[\p{L}\p{N}_][\p{L}\p{N}_.-]*$/u;
12216
+ DOTNET_ADD_VALUE_FLAGS = /* @__PURE__ */ new Set([
12217
+ "-v",
12218
+ "--version",
12219
+ "-s",
12220
+ "--source",
12221
+ "-f",
12222
+ "--framework",
12223
+ "--package-directory"
12224
+ ]);
12225
+ DOTNET_TOOL_VALUE_FLAGS = /* @__PURE__ */ new Set([
12226
+ "--version",
12227
+ "--add-source",
12228
+ "--configfile",
12229
+ "--framework",
12230
+ "--tool-path",
12231
+ "-a",
12232
+ "--arch",
12233
+ "--tool-manifest",
12234
+ "-v",
12235
+ "--verbosity"
12236
+ ]);
12237
+ NUGET_VALUE_FLAGS = /* @__PURE__ */ new Set([
12238
+ "-version",
12239
+ "-outputdirectory",
12240
+ "-o",
12241
+ "-source",
12242
+ "-configfile",
12243
+ "-fallbacksource",
12244
+ "-framework",
12245
+ "-packagesavemode",
12246
+ "-dependencyversion",
12247
+ "-solutiondirectory",
12248
+ "-verbosity"
12249
+ ]);
12250
+ MAVEN_COORD_RE = /^[\p{L}\p{N}_.-]+:[\p{L}\p{N}_.-]+$/u;
12251
+ COMPOSER_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_.-]*\/[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
12252
+ COMPOSER_VALUE_FLAGS = /* @__PURE__ */ new Set([
12253
+ "-d",
12254
+ "--working-dir",
12255
+ "--stability",
12256
+ "--repository",
12257
+ "--add-repository"
12258
+ ]);
12259
+ EDITOR_HEADS = ["code", "code-insiders", "codium", "vscodium", "cursor"];
12260
+ EXTENSION_ID_RE = /^[\p{L}\p{N}][\p{L}\p{N}_-]*\.[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
11423
12261
  MANAGER_HEADS = /* @__PURE__ */ new Set([
12262
+ "gem",
12263
+ "bundle",
12264
+ "cargo",
12265
+ "go",
12266
+ "dotnet",
12267
+ "nuget",
12268
+ "mvn",
12269
+ "mvnw",
12270
+ "composer",
12271
+ ...EDITOR_HEADS,
11424
12272
  "npm",
11425
12273
  "npx",
11426
12274
  "pnpm",
@@ -11499,6 +12347,7 @@ zoo`.split("\n");
11499
12347
  MAP_GOVERNED = /* @__PURE__ */ new Set([
11500
12348
  "files.shell-startup",
11501
12349
  "files.password-hashes",
12350
+ "files.system-dirs",
11502
12351
  "commands.inline-exec",
11503
12352
  "commands.eval-dynamic",
11504
12353
  "commands.curl-pipe-shell",
@@ -11767,7 +12616,6 @@ zoo`.split("\n");
11767
12616
  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/;
11768
12617
  PII_CC15_RE = /\b3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}\b/;
11769
12618
  PII_IBAN_RE = /\b[A-Z]{2}\d{2}(?:[A-Z0-9]|[ -][A-Z0-9]){11,30}\b/;
11770
- NUMBER_LEAD_RE = /[\s:=;([{"'+-]/;
11771
12619
  REALTIME_PII_PATTERNS = ["SSN", "Credit Card", "IBAN"];
11772
12620
  MAX_PII_SCAN_BYTES = 1e5;
11773
12621
  MASKED_KINDS = /* @__PURE__ */ new Set(["email", "iban", "account"]);
@@ -14629,14 +15477,23 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
14629
15477
  clearTimeout(timer);
14630
15478
  }
14631
15479
  }
14632
- async function waitForDaemonDecision(id, signal) {
15480
+ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
14633
15481
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
14634
15482
  const waitCtrl = new AbortController();
14635
- const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
15483
+ let capped = false;
15484
+ const waitTimer = maxWaitMs > 0 ? setTimeout(() => {
15485
+ capped = true;
15486
+ waitCtrl.abort();
15487
+ }, maxWaitMs) : void 0;
14636
15488
  const onAbort = () => waitCtrl.abort();
14637
15489
  if (signal) signal.addEventListener("abort", onAbort);
14638
15490
  try {
14639
- const waitRes = await fetch(`${base2}/wait/${id}`, { signal: waitCtrl.signal });
15491
+ const waitRes = await fetch(`${base2}/wait/${id}`, { signal: waitCtrl.signal }).catch(
15492
+ (err) => {
15493
+ if (capped && !signal?.aborted) throw new Error("node9 daemon did not answer in time");
15494
+ throw err;
15495
+ }
15496
+ );
14640
15497
  if (!waitRes.ok) return { decision: "deny" };
14641
15498
  const { decision, source, reason } = await waitRes.json();
14642
15499
  if (decision === "allow") return { decision: "allow", source };
@@ -15677,13 +16534,13 @@ function maskRiskMetadataForCloud(rm) {
15677
16534
  const r = rm;
15678
16535
  const dests = Array.isArray(r.exposureDestinations) ? r.exposureDestinations.filter((d) => typeof d === "string") : [];
15679
16536
  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);
15680
- const maskText = (text) => {
16537
+ const maskText2 = (text) => {
15681
16538
  let out2 = text;
15682
16539
  for (const [full, masked] of swaps) out2 = out2.split(full).join(masked);
15683
16540
  return maskPersonalData(out2);
15684
16541
  };
15685
16542
  const out = {};
15686
- for (const [k, v] of Object.entries(r)) out[k] = typeof v === "string" ? maskText(v) : v;
16543
+ for (const [k, v] of Object.entries(r)) out[k] = typeof v === "string" ? maskText2(v) : v;
15687
16544
  if (dests.length > 0) {
15688
16545
  out.exposureDestinations = dests.map(maskDestination);
15689
16546
  out.exposureDestinationHashes = dests.map((d) => isMaskedKind(d) ? hashDestination(d) : "");
@@ -15886,7 +16743,17 @@ function ecoDir(eco) {
15886
16743
  return import_path23.default.join(osvRoot(), eco);
15887
16744
  }
15888
16745
  function indexKey(eco, name) {
15889
- return eco === "PyPI" ? normalizePyPiName(name) : name.toLowerCase();
16746
+ switch (eco) {
16747
+ case "PyPI":
16748
+ return normalizePyPiName(name);
16749
+ case "crates.io":
16750
+ return name.toLowerCase().replace(/_/g, "-");
16751
+ case "Go":
16752
+ case "Maven":
16753
+ return name;
16754
+ default:
16755
+ return name.toLowerCase();
16756
+ }
15890
16757
  }
15891
16758
  function shardOf(key) {
15892
16759
  return (0, import_crypto9.createHash)("sha1").update(key).digest("hex").slice(0, 2);
@@ -16097,8 +16964,11 @@ async function pypiInfo(name, version) {
16097
16964
  publishedAtMs: times.length > 0 ? Math.min(...times) : void 0
16098
16965
  };
16099
16966
  }
16967
+ var HAS_REGISTRY_CLIENT = /* @__PURE__ */ new Set(["npm", "PyPI"]);
16100
16968
  async function registryInfo(eco, name, version, freshWindowMs, raw) {
16101
- return eco === "npm" ? npmInfo(name, version, freshWindowMs, raw) : pypiInfo(name, version);
16969
+ if (eco === "npm") return npmInfo(name, version, freshWindowMs, raw);
16970
+ if (eco === "PyPI") return pypiInfo(name, version);
16971
+ return null;
16102
16972
  }
16103
16973
 
16104
16974
  // src/supply-chain/local-resolve.ts
@@ -16137,26 +17007,51 @@ function commandOf(toolName, args) {
16137
17007
  const a = args;
16138
17008
  return typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : null;
16139
17009
  }
17010
+ var KIND = {
17011
+ npm: "npm package",
17012
+ PyPI: "PyPI package",
17013
+ RubyGems: "RubyGems package",
17014
+ "crates.io": "crates.io package",
17015
+ Go: "Go module",
17016
+ NuGet: "NuGet package",
17017
+ Maven: "Maven artifact",
17018
+ Packagist: "Packagist package",
17019
+ VSCode: "VS Code extension"
17020
+ };
16140
17021
  function label(p, version) {
16141
- return `${p.ecosystem === "npm" ? "npm" : "PyPI"} package "${p.name}${version ? `@${version}` : ""}"`;
17022
+ return `${KIND[p.ecosystem]} "${p.name}${version ? `@${version}` : ""}"`;
17023
+ }
17024
+ function osvNames(p) {
17025
+ if (p.ecosystem !== "Go") return [p.name];
17026
+ const parts = p.name.split("/");
17027
+ const out = [];
17028
+ for (let n = parts.length; n >= 2 && out.length < 6; n--) out.push(parts.slice(0, n).join("/"));
17029
+ return out;
16142
17030
  }
16143
17031
  async function maliciousFor(p, version, cfg, misses, networkCapped) {
16144
- const local = lookupIndex(p.ecosystem, p.name);
16145
- const entries = local.status === "hit" ? local.entries : null;
16146
- if (!entries && !(local.status === "clean" && local.fresh)) {
17032
+ const names = osvNames(p);
17033
+ const local = names.map((n) => lookupIndex(p.ecosystem, n));
17034
+ const hits2 = local.flatMap((l) => l.status === "hit" ? l.entries : []);
17035
+ const entries = hits2.length > 0 ? hits2 : null;
17036
+ const freshClean = local.every((l) => l.status === "clean" && l.fresh);
17037
+ const status = local.some((l) => l.status === "unavailable") ? "unavailable" : local[0].status;
17038
+ if (!entries && !freshClean) {
16147
17039
  if (!cfg.onlineFallback || networkCapped) {
16148
17040
  misses.push(
16149
- `${label(p, version)}: local index ${local.status}, no online lookup (${networkCapped ? "over the per-command network cap" : "online fallback off"})`
17041
+ `${label(p, version)}: local index ${status}, no online lookup (${networkCapped ? "over the per-command network cap" : "online fallback off"})`
16150
17042
  );
16151
17043
  return null;
16152
17044
  }
16153
- let ids = null;
17045
+ let perName = null;
16154
17046
  try {
16155
- ids = await queryOsvMalicious([{ ecosystem: p.ecosystem, name: p.name, version }]);
17047
+ perName = await queryOsvMalicious(
17048
+ names.map((name) => ({ ecosystem: p.ecosystem, name, version }))
17049
+ );
16156
17050
  } catch (err) {
16157
17051
  misses.push(`${label(p, version)}: OSV online ${err.message}`);
16158
17052
  return null;
16159
17053
  }
17054
+ const ids = perName ? [[...new Set(perName.flat())]] : null;
16160
17055
  if (!ids) {
16161
17056
  misses.push(`${label(p, version)}: OSV online unavailable`);
16162
17057
  return null;
@@ -16226,7 +17121,7 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
16226
17121
  }
16227
17122
  const findings = [];
16228
17123
  let info = null;
16229
- const wantsRegistry = cfg.newPackage !== "off" || cfg.installScript !== "off";
17124
+ const wantsRegistry = (cfg.newPackage !== "off" || cfg.installScript !== "off") && HAS_REGISTRY_CLIENT.has(p.ecosystem);
16230
17125
  if (wantsRegistry && !networkCapped) {
16231
17126
  try {
16232
17127
  const windowMs = cfg.newPackage === "off" ? 0 : cfg.maxAgeHours * 36e5;
@@ -16307,6 +17202,36 @@ async function runPackageCheck(toolName, args, cfg, cwd) {
16307
17202
  }
16308
17203
  }
16309
17204
 
17205
+ // src/policy/negotiation.ts
17206
+ var BLOCKED_BY = " is blocked by ";
17207
+ var DASH_BLOCKED_BY = " \u2014 blocked by ";
17208
+ var SHIELD = " shield";
17209
+ function asApprovalRequest(description) {
17210
+ const is = description.lastIndexOf(BLOCKED_BY);
17211
+ if (is > 0) {
17212
+ const by = description.slice(is + BLOCKED_BY.length).replace(/\.$/, "");
17213
+ if (by.endsWith(SHIELD)) return `${description.slice(0, is)} needs your approval (${by})`;
17214
+ }
17215
+ const dash = description.lastIndexOf(DASH_BLOCKED_BY);
17216
+ if (dash > 0) {
17217
+ const by = description.slice(dash + DASH_BLOCKED_BY.length).replace(/\.$/, "");
17218
+ if (by.endsWith(SHIELD)) return `${description.slice(0, dash)}. Needs your approval (${by})`;
17219
+ }
17220
+ return description;
17221
+ }
17222
+
17223
+ // src/auth/decision-sources.ts
17224
+ var HUMAN_DECISION_SOURCES = /* @__PURE__ */ new Set([
17225
+ "terminal",
17226
+ "browser",
17227
+ "native"
17228
+ ]);
17229
+ var REDIRECT_SOURCE = "terminal-redirect";
17230
+ var DECISION_ROUTE_SOURCES = /* @__PURE__ */ new Set([
17231
+ ...HUMAN_DECISION_SOURCES,
17232
+ REDIRECT_SOURCE
17233
+ ]);
17234
+
16310
17235
  // src/auth/orchestrator.ts
16311
17236
  var SENSITIVE_PATH_PATTERN = sensitivePathMatch("").patternName;
16312
17237
  function isProtectedPathRule(ruleName) {
@@ -16434,6 +17359,10 @@ async function authorizeHeadless(toolName, args, meta, options) {
16434
17359
  }
16435
17360
  return _authorizeHeadlessCore(toolName, args, meta, options);
16436
17361
  }
17362
+ function daemonWaitCapMs(approvalTimeoutMs) {
17363
+ if (approvalTimeoutMs === 0) return 0;
17364
+ return Math.max(approvalTimeoutMs ?? 0, 12e4) + 5e3;
17365
+ }
16437
17366
  function approvalTimeoutReason(approvalTimeoutMs) {
16438
17367
  return `No human response within ${approvalTimeoutMs / 1e3}s \u2014 auto-denied by timeout policy. Approve from your phone next time: run \`node9 login\`.`;
16439
17368
  }
@@ -16469,6 +17398,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16469
17398
  let statefulRecoveryCommand;
16470
17399
  let localSmartRuleMatched = false;
16471
17400
  let hardBlockDowngraded = false;
17401
+ let downgradedBlock = null;
16472
17402
  let argScan;
16473
17403
  const argumentScan = () => argScan ??= scanArgumentSecrets(args);
16474
17404
  let fileAccess;
@@ -17088,6 +18018,16 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
17088
18018
  humanApproverReachable,
17089
18019
  overridable: policyResult.overridable
17090
18020
  });
18021
+ const blocked = {
18022
+ approved: false,
18023
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
18024
+ blockedBy: "local-config",
18025
+ blockedByLabel: policyResult.blockedByLabel,
18026
+ ruleHit: policyResult.ruleName,
18027
+ ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
18028
+ ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription },
18029
+ ...protectedPathFields(policyResult.ruleName, toolName, args, dlpReviewFlagged)
18030
+ };
17091
18031
  const hardBlock = () => {
17092
18032
  if (!isManual)
17093
18033
  appendLocalAudit(
@@ -17098,17 +18038,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
17098
18038
  { ...meta, ruleName: policyResult.ruleName },
17099
18039
  hashAuditArgs
17100
18040
  );
17101
- return {
17102
- approved: false,
17103
- reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
17104
- blockedBy: "local-config",
17105
- blockedByLabel: policyResult.blockedByLabel,
17106
- ruleHit: policyResult.ruleName,
17107
- ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
17108
- ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription },
17109
- ...protectedPathFields(policyResult.ruleName, toolName, args, dlpReviewFlagged)
17110
- };
18041
+ return blocked;
17111
18042
  };
18043
+ downgradedBlock = blocked;
17112
18044
  if (policyResult.dependsOnStatePredicates?.length) {
17113
18045
  const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
17114
18046
  const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
@@ -17165,7 +18097,8 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
17165
18097
  policyMatchedWord,
17166
18098
  policyResult.ruleName
17167
18099
  );
17168
- if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
18100
+ if (policyRuleDescription)
18101
+ riskMetadata.ruleDescription = (hardBlockDowngraded ? asApprovalRequest(policyRuleDescription) : policyRuleDescription).slice(0, 200);
17169
18102
  const persistent = policyResult.ruleName || policyResult.tier === 7 || hardBlockDowngraded ? null : getPersistentDecision(toolName);
17170
18103
  if (persistent === "allow" && !callSpecificReview()) {
17171
18104
  if (!isManual)
@@ -17329,7 +18262,7 @@ ${packageReview}`;
17329
18262
  }
17330
18263
  }
17331
18264
  if (initResult.pending) cloudRequestId = initResult.requestId || null;
17332
- if (!callSpecificReview()) explainableLabel = "Organization Policy (SaaS)";
18265
+ if (!reviewDemanded()) explainableLabel = "Organization Policy (SaaS)";
17333
18266
  } catch {
17334
18267
  }
17335
18268
  }
@@ -17471,10 +18404,54 @@ ${packageReview}`;
17471
18404
  decision: daemonDecision,
17472
18405
  source: decisionSource,
17473
18406
  reason: daemonReason
17474
- } = await waitForDaemonDecision(daemonEntryId, signal);
18407
+ } = await waitForDaemonDecision(
18408
+ daemonEntryId,
18409
+ signal,
18410
+ daemonWaitCapMs(config.settings.approvalTimeoutMs)
18411
+ );
17475
18412
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
17476
18413
  const isApproved = daemonDecision === "allow";
17477
- const isRedirect = decisionSource === "terminal-redirect";
18414
+ const isRedirect = decisionSource === REDIRECT_SOURCE;
18415
+ if (!isApproved && !isRedirect && !(decisionSource && HUMAN_DECISION_SOURCES.has(decisionSource))) {
18416
+ if (decisionSource === "cloud")
18417
+ return {
18418
+ approved: false,
18419
+ reason: daemonReason || "Action rejected by organization administrator via Slack.",
18420
+ blockedBy: "team-policy",
18421
+ blockedByLabel: "Organization Policy (SaaS)",
18422
+ decisionSource: "cloud"
18423
+ };
18424
+ const timedOut = decisionSource === "timeout";
18425
+ if (downgradedBlock)
18426
+ return {
18427
+ ...downgradedBlock,
18428
+ blockedBy: timedOut ? "timeout" : "local-config"
18429
+ };
18430
+ if (timedOut)
18431
+ return {
18432
+ approved: false,
18433
+ reason: daemonReason || "No response: auto-denied after the approval timeout.",
18434
+ blockedBy: "timeout",
18435
+ blockedByLabel: "Approval Timeout"
18436
+ };
18437
+ if (decisionSource === "policy")
18438
+ return {
18439
+ approved: false,
18440
+ reason: daemonReason || "Action blocked by policy.",
18441
+ blockedBy: "local-config",
18442
+ blockedByLabel: explainableLabel
18443
+ };
18444
+ return {
18445
+ approved: false,
18446
+ reason: daemonReason || "node9 could not get an approval decision for this call.",
18447
+ blockedBy: "no-approval-mechanism",
18448
+ blockedByLabel: "Approval Unavailable"
18449
+ };
18450
+ }
18451
+ if (isApproved && decisionSource === "cloud")
18452
+ return { approved: true, checkedBy: "cloud", decisionSource: "cloud" };
18453
+ if (isApproved && decisionSource === "policy")
18454
+ return { approved: true, checkedBy: "local-policy" };
17478
18455
  const via = "Terminal (node9 tail)";
17479
18456
  return {
17480
18457
  approved: isApproved,