@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.mjs CHANGED
@@ -1173,6 +1173,120 @@ function expandPlainParam(p) {
1173
1173
  if (recorded !== void 0) return recorded;
1174
1174
  return HOME_VARIABLES.has(p.Param?.Value) ? "~" : void 0;
1175
1175
  }
1176
+ function functionBodies(f) {
1177
+ const out = /* @__PURE__ */ new Map();
1178
+ syntax.Walk(f, (node) => {
1179
+ const n = node;
1180
+ if (n && syntax.NodeType(n) === "FuncDecl" && n.Name?.Value && n.Body) {
1181
+ const list = out.get(n.Name.Value) ?? [];
1182
+ list.push(n.Body);
1183
+ out.set(n.Name.Value, list);
1184
+ }
1185
+ return true;
1186
+ });
1187
+ return out;
1188
+ }
1189
+ function rebindsPositionals(body) {
1190
+ let found = false;
1191
+ syntax.Walk(body, (node) => {
1192
+ const n = node;
1193
+ if (found || !n) return false;
1194
+ if (syntax.NodeType(n) === "CallExpr") {
1195
+ const words = (n.Args || []).map((a) => resolveWordLiteral(a));
1196
+ if (words[0] === "shift") found = true;
1197
+ else if (words[0] === "set" && words.slice(1).some((w) => w === null || w === "--" || !/^[-+]/.test(w)))
1198
+ found = true;
1199
+ }
1200
+ return !found;
1201
+ });
1202
+ return found;
1203
+ }
1204
+ function tableBindings() {
1205
+ if (!assignmentTable) return "";
1206
+ const out = [];
1207
+ for (const [name, rec] of assignmentTable)
1208
+ if (rec.at < currentStmtOffset) out.push(`${name}=${rec.value ?? "\0"}`);
1209
+ return out.sort().join("");
1210
+ }
1211
+ function walkWithCalls(f, visit) {
1212
+ const bodies = functionBodies(f);
1213
+ if (bodies.size === 0) {
1214
+ syntax.Walk(f, visit);
1215
+ return;
1216
+ }
1217
+ const seenCalls = /* @__PURE__ */ new Set();
1218
+ const inline = (call) => {
1219
+ const words = (call.Args || []).map((a) => resolveWordLiteral(a));
1220
+ const list = words[0] ? bodies.get(words[0]) : void 0;
1221
+ if (!list || callDepth >= MAX_CALL_DEPTH) return;
1222
+ const args = words.slice(1);
1223
+ const key = `${words[0]}\0${JSON.stringify(args)}\0${tableBindings()}`;
1224
+ if (seenCalls.has(key)) return;
1225
+ seenCalls.add(key);
1226
+ const outerTable = assignmentTable;
1227
+ const outerOffset = currentStmtOffset;
1228
+ for (const body of list) {
1229
+ if (callWalks >= MAX_CALL_WALKS) return;
1230
+ callWalks++;
1231
+ const table = /* @__PURE__ */ new Map();
1232
+ for (const [name, rec] of outerTable ?? [])
1233
+ if (rec.at < outerOffset) table.set(name, { value: rec.value, at: -1 });
1234
+ for (const name of ["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*", "#"])
1235
+ table.delete(name);
1236
+ if (!rebindsPositionals(body)) {
1237
+ args.slice(0, 9).forEach((value, i) => table.set(String(i + 1), { value, at: -1 }));
1238
+ if (args.length === 1) {
1239
+ table.set("@", { value: args[0], at: -1 });
1240
+ table.set("*", { value: args[0], at: -1 });
1241
+ }
1242
+ table.set("#", { value: String(args.length), at: -1 });
1243
+ }
1244
+ assignmentTable = table;
1245
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1246
+ callDepth++;
1247
+ try {
1248
+ if (body.Cmd && syntax.NodeType(body.Cmd) === "Block") recordTopLevelAssignments(body.Cmd);
1249
+ syntax.Walk(body, step);
1250
+ } finally {
1251
+ callDepth--;
1252
+ assignmentTable = outerTable;
1253
+ currentStmtOffset = outerOffset;
1254
+ }
1255
+ }
1256
+ };
1257
+ const step = (node) => {
1258
+ if (!node) return false;
1259
+ const go = visit(node);
1260
+ if (go !== false && syntax.NodeType(node) === "CallExpr") inline(node);
1261
+ return go;
1262
+ };
1263
+ syntax.Walk(f, step);
1264
+ }
1265
+ function withAssignments(f, body) {
1266
+ const outerTable = assignmentTable;
1267
+ const outerOffset = currentStmtOffset;
1268
+ const outerWalks = callWalks;
1269
+ callWalks = 0;
1270
+ assignmentTable = new Map(
1271
+ [...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
1272
+ );
1273
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1274
+ try {
1275
+ recordTopLevelAssignments(f);
1276
+ return body();
1277
+ } finally {
1278
+ assignmentTable = outerTable;
1279
+ currentStmtOffset = outerOffset;
1280
+ callWalks = outerWalks;
1281
+ }
1282
+ }
1283
+ function trackStatement(n) {
1284
+ try {
1285
+ currentStmtOffset = n.Pos().Offset();
1286
+ } catch {
1287
+ currentStmtOffset = Number.MAX_SAFE_INTEGER;
1288
+ }
1289
+ }
1176
1290
  function resolveWordLiteral(w) {
1177
1291
  const parts = w?.Parts || [];
1178
1292
  let s = "";
@@ -1293,27 +1407,31 @@ function extractShellDestinations(command) {
1293
1407
  const out = [];
1294
1408
  const seen = /* @__PURE__ */ new Set();
1295
1409
  try {
1296
- syntax.Walk(f, (node) => {
1297
- if (!node) return false;
1298
- const n = node;
1299
- if (syntax.NodeType(n) !== "CallExpr") return true;
1300
- const callArgs = n.Args || [];
1301
- if (callArgs.length === 0) return true;
1302
- const words = callArgs.map((a) => resolveWordLiteral(a));
1303
- const at = unwrapCommandHead(words);
1304
- const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
1305
- if (!NET_BINARIES.has(name)) return true;
1306
- const rest = words.slice(at + 1);
1307
- for (const raw of destTokensForBinary(name, rest)) {
1308
- const host2 = parseDestHost(raw);
1309
- if (!host2) continue;
1310
- const key = `${name}:${host2}`;
1311
- if (seen.has(key)) continue;
1312
- seen.add(key);
1313
- out.push({ host: host2, binary: name, raw });
1314
- }
1315
- return true;
1316
- });
1410
+ withAssignments(
1411
+ f,
1412
+ () => walkWithCalls(f, (node) => {
1413
+ if (!node) return false;
1414
+ const n = node;
1415
+ if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1416
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1417
+ const callArgs = n.Args || [];
1418
+ if (callArgs.length === 0) return true;
1419
+ const words = callArgs.map((a) => resolveWordLiteral(a));
1420
+ const at = unwrapCommandHead(words);
1421
+ const name = (words[at] || "").toLowerCase().split("/").pop() ?? "";
1422
+ if (!NET_BINARIES.has(name)) return true;
1423
+ const rest = words.slice(at + 1);
1424
+ for (const raw of destTokensForBinary(name, rest)) {
1425
+ const host2 = parseDestHost(raw);
1426
+ if (!host2) continue;
1427
+ const key = `${name}:${host2}`;
1428
+ if (seen.has(key)) continue;
1429
+ seen.add(key);
1430
+ out.push({ host: host2, binary: name, raw });
1431
+ }
1432
+ return true;
1433
+ })
1434
+ );
1317
1435
  } catch {
1318
1436
  return out;
1319
1437
  }
@@ -1325,39 +1443,45 @@ function extractShellDestTokens(command) {
1325
1443
  const out = [];
1326
1444
  const seen = /* @__PURE__ */ new Set();
1327
1445
  try {
1328
- syntax.Walk(f, (node) => {
1329
- if (!node) return false;
1330
- const n = node;
1331
- if (syntax.NodeType(n) !== "CallExpr") return true;
1332
- const callArgs = n.Args || [];
1333
- if (callArgs.length === 0) return true;
1334
- const name = (resolveWordLiteral(callArgs[0]) || "").toLowerCase();
1335
- if (!NET_BINARIES.has(name)) return true;
1336
- const rest = callArgs.slice(1).map((a) => resolveWordLiteral(a));
1337
- for (const raw of destTokensForBinary(name, rest)) {
1338
- if (!raw) continue;
1339
- let tok = raw.trim();
1340
- const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(tok);
1341
- const hasScheme = scheme !== null;
1342
- if (hasScheme) tok = tok.slice(scheme[0].length);
1343
- tok = tok.split(/[/?#]/)[0];
1344
- const at = tok.lastIndexOf("@");
1345
- if (at >= 0) tok = tok.slice(at + 1);
1346
- if (tok.startsWith("[")) {
1347
- const close = tok.indexOf("]");
1348
- if (close > 0) tok = tok.slice(0, close + 1);
1349
- } else {
1350
- tok = tok.split(":")[0];
1446
+ withAssignments(
1447
+ f,
1448
+ () => walkWithCalls(f, (node) => {
1449
+ if (!node) return false;
1450
+ const n = node;
1451
+ if (syntax.NodeType(n) === "Stmt") trackStatement(n);
1452
+ if (syntax.NodeType(n) !== "CallExpr") return true;
1453
+ const callArgs = n.Args || [];
1454
+ if (callArgs.length === 0) return true;
1455
+ const words = callArgs.map((a) => resolveWordLiteral(a));
1456
+ const head = unwrapCommandHead(words);
1457
+ const name = (words[head] || "").toLowerCase().split("/").pop() ?? "";
1458
+ if (!NET_BINARIES.has(name)) return true;
1459
+ const rest = words.slice(head + 1);
1460
+ for (const raw of destTokensForBinary(name, rest)) {
1461
+ if (!raw) continue;
1462
+ let tok = raw.trim();
1463
+ const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(tok);
1464
+ const hasScheme = scheme !== null;
1465
+ if (hasScheme) tok = tok.slice(scheme[0].length);
1466
+ tok = tok.split(/[/?#]/)[0];
1467
+ const at = tok.lastIndexOf("@");
1468
+ if (at >= 0) tok = tok.slice(at + 1);
1469
+ if (tok.startsWith("[")) {
1470
+ const close = tok.indexOf("]");
1471
+ if (close > 0) tok = tok.slice(0, close + 1);
1472
+ } else {
1473
+ tok = tok.split(":")[0];
1474
+ }
1475
+ if (!tok) continue;
1476
+ if (!hasScheme && /^\d+$/.test(tok) && Number(tok) < 16777216) continue;
1477
+ const key = `${name}:${tok}`;
1478
+ if (seen.has(key)) continue;
1479
+ seen.add(key);
1480
+ out.push({ token: tok, binary: name });
1351
1481
  }
1352
- if (!tok) continue;
1353
- if (!hasScheme && /^\d+$/.test(tok) && Number(tok) < 16777216) continue;
1354
- const key = `${name}:${tok}`;
1355
- if (seen.has(key)) continue;
1356
- seen.add(key);
1357
- out.push({ token: tok, binary: name });
1358
- }
1359
- return true;
1360
- });
1482
+ return true;
1483
+ })
1484
+ );
1361
1485
  } catch {
1362
1486
  return out;
1363
1487
  }
@@ -1476,6 +1600,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1476
1600
  if (depth === 0) {
1477
1601
  seenPayloads = /* @__PURE__ */ new Set();
1478
1602
  payloadBudget = PAYLOAD_BUDGET;
1603
+ if (callDepth === 0) callWalks = 0;
1479
1604
  }
1480
1605
  assignmentTable = new Map(
1481
1606
  [...outerTable ?? []].map(([k, r]) => [k, { value: r.value, at: -1 }])
@@ -1483,7 +1608,7 @@ function analyzeFsOperationImpl(command, depth = 0) {
1483
1608
  currentStmtOffset = Number.MAX_SAFE_INTEGER;
1484
1609
  try {
1485
1610
  recordTopLevelAssignments(f);
1486
- syntax.Walk(f, (node) => {
1611
+ walkWithCalls(f, (node) => {
1487
1612
  if (!node || result?.verdict === "block") return false;
1488
1613
  const n = node;
1489
1614
  const nodeType = syntax.NodeType(n);
@@ -2272,6 +2397,19 @@ function runnablePrefix(command) {
2272
2397
  }
2273
2398
  return null;
2274
2399
  }
2400
+ function uploadedFile(tool, flag, value) {
2401
+ const v = value.value;
2402
+ const file2 = (f) => f && f !== "-" && f !== "." ? { value: f } : null;
2403
+ if (tool === "curl") {
2404
+ if (["-d", "--data", "--data-ascii", "--data-binary", "--json"].includes(flag))
2405
+ return v.startsWith("@") ? file2(v.slice(1)) : null;
2406
+ if (flag === "--data-urlencode") return file2(/^[^=@]*@(.+)$/.exec(v)?.[1]);
2407
+ if (flag === "-F" || flag === "--form") return file2(/^[^=]*=[@<]([^;]+)/.exec(v)?.[1]);
2408
+ if (flag === "-T" || flag === "--upload-file") return file2(v);
2409
+ }
2410
+ if (tool === "wget" && ["--post-file", "--body-file"].includes(flag)) return file2(v);
2411
+ return null;
2412
+ }
2275
2413
  function merge(a, b) {
2276
2414
  a.cwd = unique([...a.cwd, ...b.cwd]);
2277
2415
  for (const key of /* @__PURE__ */ new Set([...a.vars.keys(), ...b.vars.keys()])) {
@@ -2345,6 +2483,9 @@ function wordValues(word, state) {
2345
2483
  function collectShellFileAccesses(command, context = {}) {
2346
2484
  const out = [];
2347
2485
  let payloads = 256;
2486
+ const functions = /* @__PURE__ */ new Map();
2487
+ const seenCalls = /* @__PURE__ */ new Set();
2488
+ let callWalks2 = MAX_CALL_WALKS2;
2348
2489
  const initial = {
2349
2490
  cwd: [context.cwd ?? ""],
2350
2491
  vars: /* @__PURE__ */ new Map([["HOME", [context.homeDirs?.[0] ?? "~"]]])
@@ -2602,8 +2743,98 @@ function collectShellFileAccesses(command, context = {}) {
2602
2743
  }
2603
2744
  return;
2604
2745
  }
2746
+ if (name === "wget" || name === "curl" || name === "aria2c") {
2747
+ downloadOutputs(name, args, state);
2748
+ return;
2749
+ }
2750
+ if (["http", "https", "xh", "xhs"].includes(name)) {
2751
+ for (const token of args) {
2752
+ if (!token || token.dynamic || /^[a-z][a-z0-9+.-]*:\/\//i.test(token.value)) continue;
2753
+ const m = /^(?:@|[^=:@\s]+@|[^=:@\s]*=@)(.+)$/.exec(token.value);
2754
+ if (m && m[1] !== "-") add("read", { value: m[1] }, state, { upload: true });
2755
+ }
2756
+ return;
2757
+ }
2758
+ if (name === "scp" || name === "rsync") {
2759
+ const operands2 = strings.filter((s) => s && !s.startsWith("-"));
2760
+ const remote = operands2.slice(1).some((o) => /^[^/\s]+:/.test(o));
2761
+ for (const p of copySourcePaths(words))
2762
+ add("read", locate(p), state, remote ? { upload: true } : {});
2763
+ return;
2764
+ }
2605
2765
  for (const p of copySourcePaths(words)) add("read", locate(p), state);
2606
2766
  }
2767
+ function downloadOutputs(name, args, state) {
2768
+ const valueShort = DOWNLOAD_SHORT_VALUE[name];
2769
+ const outputs = [];
2770
+ const urls = [];
2771
+ let dir2 = null;
2772
+ let remoteName = false;
2773
+ let document = false;
2774
+ let serverNamed = false;
2775
+ const take = (flag, value) => {
2776
+ if (!value) return;
2777
+ const sent = uploadedFile(name, flag, value);
2778
+ if (sent) add("read", sent, state, { upload: true });
2779
+ if (DOWNLOAD_OUTPUT_FLAGS[name].includes(flag)) {
2780
+ if (["-O", "--output-document", "--out"].includes(flag) || name === "aria2c" && flag === "-o")
2781
+ document = true;
2782
+ if (value.value !== "-" || value.pattern || value.dynamic) outputs.push(value);
2783
+ } else if (DOWNLOAD_DIR_FLAGS[name].includes(flag)) dir2 = value;
2784
+ else if (name === "wget" && (flag === "-i" || flag === "--input-file")) serverNamed = true;
2785
+ };
2786
+ for (let i = 0; i < args.length; i++) {
2787
+ const token = args[i];
2788
+ if (!token) continue;
2789
+ const v = token.value;
2790
+ if (v.startsWith("--")) {
2791
+ const eq = v.indexOf("=");
2792
+ const flag = eq < 0 ? v : v.slice(0, eq);
2793
+ if (["--remote-name", "--remote-name-all"].includes(flag)) remoteName = true;
2794
+ if (["--remote-header-name", "--recursive", "--mirror", "--page-requisites"].includes(flag))
2795
+ serverNamed = true;
2796
+ if (DOWNLOAD_LONG_VALUE[name].includes(flag))
2797
+ take(flag, eq < 0 ? args[++i] ?? null : { ...token, value: v.slice(eq + 1) });
2798
+ continue;
2799
+ }
2800
+ if (v.startsWith("-") && v.length > 1 && !token.dynamic) {
2801
+ for (let j = 1; j < v.length; j++) {
2802
+ const c = v[j];
2803
+ if (name === "wget" && c === "n") {
2804
+ j++;
2805
+ continue;
2806
+ }
2807
+ if (name === "curl" && c === "O") remoteName = true;
2808
+ if (name === "curl" && c === "J") serverNamed = true;
2809
+ if (name === "wget" && (c === "r" || c === "m" || c === "p")) serverNamed = true;
2810
+ if (valueShort.includes(c)) {
2811
+ const rest = v.slice(j + 1);
2812
+ take(`-${c}`, rest ? { value: rest } : args[++i] ?? null);
2813
+ break;
2814
+ }
2815
+ }
2816
+ continue;
2817
+ }
2818
+ if (/^(?:https?|ftps?):\/\//i.test(v)) urls.push(v);
2819
+ }
2820
+ for (const out2 of outputs) {
2821
+ const spelled = name === "aria2c" && dir2?.value && !out2.value.startsWith("/") ? { ...out2, value: `${dir2.value.replace(/\/+$/, "")}/${out2.value}` } : out2;
2822
+ if (name === "curl" && /#\d/.test(spelled.value))
2823
+ add("write", { value: "", pattern: spelled.value.replace(/#\d+/g, "*"), dot: true }, state);
2824
+ else add("write", spelled, state);
2825
+ }
2826
+ const remote = name === "curl" ? remoteName || serverNamed : !document || serverNamed;
2827
+ if (!remote) return;
2828
+ const into = dir2?.value ? dir2.value.replace(/\/+$/, "") : ".";
2829
+ if (serverNamed || urls.length === 0) {
2830
+ add("write", { value: "", pattern: `${quoteGlob(into)}/*`, dot: true }, state);
2831
+ return;
2832
+ }
2833
+ for (const url of urls) {
2834
+ const file2 = url.replace(/[?#].*$/, "").replace(/^[a-z]+:\/\/[^/]*/i, "").split("/").pop();
2835
+ add("write", { value: `${into}/${file2 || "index.html"}` }, state);
2836
+ }
2837
+ }
2607
2838
  function findActions2(tokens, words, head, state, nested, depth) {
2608
2839
  const starts = findStartPoints(words, head).starts;
2609
2840
  const roots = (starts.length ? starts : ["."]).slice(0, MAX_ALTERNATIVES);
@@ -2675,7 +2906,35 @@ function collectShellFileAccesses(command, context = {}) {
2675
2906
  const words = wordValues(node, state);
2676
2907
  alternatives = alternatives.flatMap((prefix) => (words.length ? words : [null]).map((w) => [...prefix, w])).slice(0, MAX_ALTERNATIVES);
2677
2908
  }
2678
- for (const tokens of alternatives) fromWords(tokens, state, nested, depth);
2909
+ for (const tokens of alternatives) {
2910
+ fromWords(tokens, state, nested, depth);
2911
+ inlineCall(tokens, state, depth);
2912
+ }
2913
+ }
2914
+ function inlineCall(tokens, state, depth) {
2915
+ const head = tokens[0];
2916
+ if (!head || head.dynamic || head.pattern) return;
2917
+ const bodies = functions.get(head.value);
2918
+ if (!bodies || (state.callDepth ?? 0) >= MAX_CALL_DEPTH2) return;
2919
+ const args = tokens.slice(1).map((a) => a && !a.dynamic ? a.value : null);
2920
+ const vars = JSON.stringify([...state.vars].sort(([a], [b]) => a < b ? -1 : 1));
2921
+ const key = `${head.value}\0${JSON.stringify(args)}\0${state.cwd.join("")}\0${vars}`;
2922
+ if (seenCalls.has(key)) return;
2923
+ seenCalls.add(key);
2924
+ for (const body of bodies) {
2925
+ if (callWalks2-- <= 0) return;
2926
+ const local = clone(state);
2927
+ local.callDepth = (state.callDepth ?? 0) + 1;
2928
+ for (const name of POSITIONAL) local.vars.delete(name);
2929
+ if (!rebindsPositionals(body)) {
2930
+ args.slice(0, 9).forEach((v, i) => local.vars.set(String(i + 1), v === null ? null : [v]));
2931
+ const all = args.every((v) => v !== null) && args.length ? unique(args) : null;
2932
+ local.vars.set("@", all);
2933
+ local.vars.set("*", all);
2934
+ local.vars.set("#", [String(args.length)]);
2935
+ }
2936
+ statement(body, local, depth + 1);
2937
+ }
2679
2938
  }
2680
2939
  function readsScriptFromStdin(stmt) {
2681
2940
  const cmd = stmt?.Cmd;
@@ -2712,11 +2971,13 @@ function collectShellFileAccesses(command, context = {}) {
2712
2971
  if (!stmt || depth > MAX_DEPTH) return;
2713
2972
  state.onSuccess = void 0;
2714
2973
  if (stmt.Background) state = clone(state);
2974
+ const headWord = stmt.Cmd && syntax2.NodeType(stmt.Cmd) === "CallExpr" ? base(resolveWordLiteral(stmt.Cmd.Args?.[0])) : "";
2975
+ const sends = SENDING_STDIN.has(headWord);
2715
2976
  for (const redir of stmt.Redirs ?? []) {
2716
2977
  substitutions(redir.Word, state, depth);
2717
2978
  for (const word of wordValues(redir.Word, state)) {
2718
2979
  if (WRITES.has(redir.Op)) add("write", word, state);
2719
- if (READS.has(redir.Op)) add("read", word, state);
2980
+ if (READS.has(redir.Op)) add("read", word, state, sends ? { upload: true } : {});
2720
2981
  if (redir.Op === DUP_OUT && word.value && !/^\d+$/.test(word.value))
2721
2982
  add("write", word, state);
2722
2983
  }
@@ -2825,7 +3086,10 @@ function collectShellFileAccesses(command, context = {}) {
2825
3086
  }
2826
3087
  const parsed = parseShared(command);
2827
3088
  const program = typeof parsed !== "symbol" ? parsed : runnablePrefix(command);
2828
- if (program) statements(program.Stmts ?? [], initial, 0);
3089
+ if (program) {
3090
+ for (const [name, bodies] of functionBodies(program)) functions.set(name, bodies);
3091
+ statements(program.Stmts ?? [], initial, 0);
3092
+ }
2829
3093
  return out;
2830
3094
  }
2831
3095
  function isAgentInstructionPath(absPath, root) {
@@ -2941,6 +3205,33 @@ function hits(access, paths, directories, context) {
2941
3205
  }
2942
3206
  return false;
2943
3207
  }
3208
+ function hitsSystemDir(access, context, dirs = SYSTEM_DIRS) {
3209
+ const home = context.homeDirs?.[0] ?? "~";
3210
+ const candidates = access.candidates ?? [
3211
+ normalizeFilePath(access.path, access.cwd ?? context.cwd, home)
3212
+ ];
3213
+ for (const p of candidates) {
3214
+ if (dirs.some((d) => within(p, d)) && !exempt(p)) return true;
3215
+ if (access.recursive && dirs.some((d) => within(d, p))) return true;
3216
+ }
3217
+ if (!access.glob) return false;
3218
+ const pattern = normalizeFilePath(access.glob, access.cwd ?? context.cwd, home);
3219
+ const magic = pattern.search(/[*?[{]/);
3220
+ const literal = magic < 0 ? pattern : pattern.slice(0, pattern.lastIndexOf("/", magic) + 1);
3221
+ if (literal && exempt(literal.replace(/\/$/, "") || "/")) return false;
3222
+ const options = { dot: access.dotGlob === true, nonegate: true };
3223
+ const components = pattern.split("/");
3224
+ for (const root of dirs) {
3225
+ const count = root.split("/").length;
3226
+ if (components.length > count && picomatch(components.slice(0, count).join("/"), options)(root))
3227
+ return true;
3228
+ if (access.recursive && picomatch(pattern, options)(root)) return true;
3229
+ }
3230
+ return false;
3231
+ }
3232
+ function uploadDirs(context) {
3233
+ return context.homeDirs?.[0] === "/root" ? UPLOAD_SYSTEM_DIRS.filter((d) => d !== "/root") : UPLOAD_SYSTEM_DIRS;
3234
+ }
2944
3235
  function evaluateProtectedFileAccess(accesses, context, settings) {
2945
3236
  const homes = [.../* @__PURE__ */ new Set([...context.homeDirs ?? ["~"], "/root"])];
2946
3237
  const startup = homes.flatMap(
@@ -2962,6 +3253,14 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
2962
3253
  "files.shell-startup",
2963
3254
  "review-shell-startup-mutation",
2964
3255
  "Changing shell startup files requires approval"
3256
+ ] : mutation && hitsSystemDir(access, context) ? [
3257
+ "files.system-dirs",
3258
+ "review-system-dir-mutation",
3259
+ "Changing files in a system directory requires approval"
3260
+ ] : !mutation && access.upload && hitsSystemDir(access, context, uploadDirs(context)) ? [
3261
+ "files.system-dirs",
3262
+ "review-system-file-upload",
3263
+ "Uploading a system file requires approval"
2965
3264
  ] : !mutation && hits(access, PASSWORD_HASH_FILES, [], context) ? [
2966
3265
  "files.password-hashes",
2967
3266
  "review-password-hash-read",
@@ -3222,16 +3521,21 @@ function looksGenerated(value, minLength, minEntropy, lettersOnlyAllowed = false
3222
3521
  if (EMAIL_VALUE_RE.test(value)) return false;
3223
3522
  if (looksLikeJoinedWords(value) || hasSequentialRun(value)) return false;
3224
3523
  if (PATH_VALUE_RE.test(value) || DOTTED_REFERENCE_RE.test(value)) return false;
3524
+ if (CONCATENATION_RE.test(value)) return false;
3225
3525
  if (rejectedByStructuredValidator(value)) return false;
3226
3526
  return shannonEntropy(value) >= minEntropy;
3227
3527
  }
3228
3528
  function nameHoldsCredential(name) {
3229
3529
  return !FIXTURE_NAME_RE.test(name) && !DERIVATIVE_NAME_RE.test(name);
3230
3530
  }
3531
+ function isOtpDocSecret(value) {
3532
+ return OTP_DOC_SECRETS.has(value.replace(/=+$/, "").toUpperCase());
3533
+ }
3231
3534
  function validateQueryCredential(raw) {
3232
3535
  const value = capturedValue(raw, QUERY_CREDENTIAL_VALUE_RE);
3233
3536
  if (value === null) return false;
3234
- if (OTP_SECRET_RE.test(value)) return looksGenerated(value, 16, 3, true);
3537
+ if (OTP_SECRET_RE.test(value))
3538
+ return !isOtpDocSecret(value) && looksGenerated(value, 16, 3, true);
3235
3539
  if (!/[0-9]/.test(value) || !/[A-Za-z]/.test(value)) return false;
3236
3540
  return looksGenerated(value, 12, 3);
3237
3541
  }
@@ -3245,13 +3549,40 @@ function keywordEndsAPart(name) {
3245
3549
  }
3246
3550
  function validateAssignedSecret(raw, before = "") {
3247
3551
  const value = capturedValue(raw, ASSIGNED_SECRET_VALUE_RE);
3248
- const name = capturedValue(raw, ASSIGNED_SECRET_NAME_RE);
3552
+ const name = capturedValue(raw, ASSIGNED_SECRET_NAME_RE)?.replace(/^_+/, "") ?? null;
3249
3553
  if (value === null || name === null) return false;
3250
- if (!keywordEndsAPart(name) || ARN_CONTEXT_RE.test(before)) return false;
3251
- const prefix = before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "";
3554
+ if (!keywordEndsAPart(name)) return false;
3555
+ if (ARN_CONTEXT_RE.test(before)) return false;
3556
+ const prefix = (before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "") + (raw.startsWith("_") ? "_" : "");
3252
3557
  if (!nameHoldsCredential(prefix + name)) return false;
3253
3558
  return looksGenerated(value, 12, 3.5);
3254
3559
  }
3560
+ function validateBasicCredential(raw) {
3561
+ const b64 = capturedValue(raw, BASIC_VALUE_RE);
3562
+ if (b64 === null) return false;
3563
+ const bare = b64.replace(/=+$/, "");
3564
+ if (bare.length % 4 === 1) return false;
3565
+ const bytes = Buffer.from(b64, "base64");
3566
+ if (bytes.toString("base64").replace(/=+$/, "") !== bare) return false;
3567
+ const text = bytes.toString("utf8");
3568
+ if (!Buffer.from(text, "utf8").equals(bytes)) return false;
3569
+ for (let i = 0; i < text.length; i++) {
3570
+ const c = text.charCodeAt(i);
3571
+ if (c < 32 || c === 127) return false;
3572
+ }
3573
+ const pair = BASIC_PAIR_RE.exec(text);
3574
+ if (!pair) return false;
3575
+ const password = pair[2];
3576
+ if (!/[0-9]/.test(password) || !/[A-Za-z]/.test(password)) return false;
3577
+ const lower2 = password.toLowerCase();
3578
+ if (DLP_STOPWORDS.some((sw) => lower2.includes(sw)) || hasSequentialRun(password)) return false;
3579
+ return shannonEntropy(password) >= 3;
3580
+ }
3581
+ function validateOtpSecret(raw) {
3582
+ const secret = capturedValue(raw, OTP_CAPTURE_RE);
3583
+ if (secret === null || isOtpDocSecret(secret)) return false;
3584
+ return looksGenerated(secret.replace(/=+$/, ""), 16, 3, true);
3585
+ }
3255
3586
  function matchSensitivePath2(resolvedPath, originalPath, options = {}) {
3256
3587
  const normalised = resolvedPath.replace(/\\/g, "/");
3257
3588
  if (!options.systemAccessHandled && isLegacySystemPath(normalised))
@@ -3522,6 +3853,156 @@ function parsePySpec(raw) {
3522
3853
  const exact = PY_EXACT_RE.exec(m[2].trim());
3523
3854
  return { name, version: exact ? exact[1] : void 0 };
3524
3855
  }
3856
+ function exactVersion(v) {
3857
+ if (!v) return void 0;
3858
+ const m = EXACT_VERSION_RE.exec(v.trim());
3859
+ return m ? m[1] : void 0;
3860
+ }
3861
+ function push(out, ecosystem, manager, name, version, raw) {
3862
+ out.push({ ecosystem, manager, name, ...version ? { version } : {}, raw });
3863
+ }
3864
+ function gemInstall(words, from, out) {
3865
+ const dash = words.indexOf("--", from);
3866
+ const own = dash < 0 ? words : words.slice(0, dash);
3867
+ const pinned = exactVersion(flagValues(own, from, ["-v", "--version"])[0]);
3868
+ for (const raw of operands(own, from, GEM_VALUE_FLAGS)) {
3869
+ if (NON_REGISTRY_RE.test(raw) || /\.gem$/i.test(raw)) continue;
3870
+ const colon = raw.indexOf(":");
3871
+ const name = colon < 0 ? raw : raw.slice(0, colon);
3872
+ if (!GEM_NAME_RE.test(name)) continue;
3873
+ push(
3874
+ out,
3875
+ "RubyGems",
3876
+ "gem",
3877
+ name,
3878
+ colon < 0 ? pinned : exactVersion(raw.slice(colon + 1)),
3879
+ raw
3880
+ );
3881
+ }
3882
+ }
3883
+ function cargoCrates(words, from, manager, out) {
3884
+ const flags = manager === "cargo add" ? CARGO_ADD_VALUE_FLAGS : CARGO_INSTALL_VALUE_FLAGS;
3885
+ if (flagValues(words, from, ["--git", "--path", "--registry", "--index"]).length > 0) return;
3886
+ if (words.some((w) => w === "--git" || w === "--path")) return;
3887
+ const flagVersion = manager === "cargo install" ? exactVersion(flagValues(words, from, ["--version", "--vers"])[0]) : void 0;
3888
+ for (const raw of operands(words, from, flags)) {
3889
+ const at = raw.indexOf("@");
3890
+ const name = at < 0 ? raw : raw.slice(0, at);
3891
+ if (!CRATE_NAME_RE.test(name)) continue;
3892
+ push(
3893
+ out,
3894
+ "crates.io",
3895
+ manager,
3896
+ name,
3897
+ at < 0 ? flagVersion : exactVersion(raw.slice(at + 1)),
3898
+ raw
3899
+ );
3900
+ }
3901
+ }
3902
+ function goModules(words, from, manager, out) {
3903
+ let specs = operands(words, from, GO_VALUE_FLAGS);
3904
+ if (manager === "go run") specs = specs.slice(0, 1).filter((s) => s.includes("@"));
3905
+ for (const raw of specs) {
3906
+ const at = raw.indexOf("@");
3907
+ const pathPart = (at < 0 ? raw : raw.slice(0, at)).replace(/\/\.\.\.$/, "");
3908
+ const ver = at < 0 ? void 0 : raw.slice(at + 1);
3909
+ if (ver === "none") continue;
3910
+ if (!GO_PATH_RE.test(pathPart) || /\.go$/i.test(pathPart)) continue;
3911
+ const exact = ver && /^v\d/.test(ver) ? exactVersion(ver) : void 0;
3912
+ push(out, "Go", manager, pathPart, exact, raw);
3913
+ }
3914
+ }
3915
+ function dotnetAdd(words, from, out) {
3916
+ let k = -1;
3917
+ for (let i = from; i < Math.min(words.length, from + 2); i++) {
3918
+ if (lower(words[i]) === "package") {
3919
+ k = i;
3920
+ break;
3921
+ }
3922
+ }
3923
+ if (k < 0) return;
3924
+ const name = operands(words, k + 1, DOTNET_ADD_VALUE_FLAGS)[0];
3925
+ if (!name || !NUGET_NAME_RE.test(name)) return;
3926
+ const version = exactVersion(flagValues(words, k + 1, ["-v", "--version"])[0]);
3927
+ push(out, "NuGet", "dotnet add package", name, version, name);
3928
+ }
3929
+ function dotnetTool(words, from, out) {
3930
+ const name = operands(words, from, DOTNET_TOOL_VALUE_FLAGS)[0];
3931
+ if (!name || !NUGET_NAME_RE.test(name)) return;
3932
+ const version = exactVersion(flagValues(words, from, ["--version"])[0]);
3933
+ push(out, "NuGet", "dotnet tool", name, version, name);
3934
+ }
3935
+ function nugetInstall(words, from, out) {
3936
+ const lowered = words.map((w) => w !== null && w.startsWith("-") ? w.toLowerCase() : w);
3937
+ const name = operands(lowered, from, NUGET_VALUE_FLAGS)[0];
3938
+ if (!name || !NUGET_NAME_RE.test(name) || /\.config$/i.test(name)) return;
3939
+ const version = exactVersion(flagValues(lowered, from, ["-version"])[0]);
3940
+ push(out, "NuGet", "nuget install", name, version, name);
3941
+ }
3942
+ function mavenGet(words, from, out) {
3943
+ const goal = words.slice(from).some(
3944
+ (w) => w !== null && /(?:^|:)dependency:get$|^org\.apache\.maven\.plugins:maven-dependency-plugin:(?:[\w.-]+:)?get$/.test(
3945
+ w
3946
+ )
3947
+ );
3948
+ if (!goal) return;
3949
+ const prop = (key) => {
3950
+ for (let i = from; i < words.length; i++) {
3951
+ const w = words[i];
3952
+ if (w === null) continue;
3953
+ if (w.startsWith(`-D${key}=`)) return w.slice(key.length + 3);
3954
+ if (w === "-D" && words[i + 1]?.startsWith(`${key}=`))
3955
+ return words[i + 1].slice(key.length + 1);
3956
+ }
3957
+ return void 0;
3958
+ };
3959
+ const artifact = prop("artifact");
3960
+ let coord;
3961
+ let ver;
3962
+ if (artifact) {
3963
+ const parts = artifact.split(":");
3964
+ if (parts.length < 2) return;
3965
+ coord = `${parts[0]}:${parts[1]}`;
3966
+ ver = parts[2];
3967
+ } else {
3968
+ const g = prop("groupId");
3969
+ const a = prop("artifactId");
3970
+ if (!g || !a) return;
3971
+ coord = `${g}:${a}`;
3972
+ ver = prop("version");
3973
+ }
3974
+ if (!MAVEN_COORD_RE.test(coord)) return;
3975
+ const exact = ver && !/^(?:LATEST|RELEASE)$/i.test(ver) ? exactVersion(ver) : void 0;
3976
+ push(out, "Maven", "mvn dependency:get", coord, exact, artifact ?? coord);
3977
+ }
3978
+ function composerPackages(words, from, manager, out) {
3979
+ const ops = operands(words, from, COMPOSER_VALUE_FLAGS);
3980
+ const specs = manager === "composer create-project" ? ops.slice(0, 1) : ops;
3981
+ for (const raw of specs) {
3982
+ const sep = raw.search(/[:=]/);
3983
+ const name = sep < 0 ? raw : raw.slice(0, sep);
3984
+ if (!COMPOSER_NAME_RE.test(name)) continue;
3985
+ let ver = sep < 0 ? void 0 : raw.slice(sep + 1);
3986
+ if (manager === "composer create-project" && ver === void 0) ver = ops[2] ?? void 0;
3987
+ push(out, "Packagist", manager, name.toLowerCase(), exactVersion(ver), raw);
3988
+ }
3989
+ }
3990
+ function editorExtensions(words, from, out) {
3991
+ for (const raw of flagValues(words, from, ["--install-extension"])) {
3992
+ if (/\.vsix$/i.test(raw) || NON_REGISTRY_RE.test(raw)) continue;
3993
+ const at = raw.lastIndexOf("@");
3994
+ const name = at <= 0 ? raw : raw.slice(0, at);
3995
+ if (!EXTENSION_ID_RE.test(name)) continue;
3996
+ push(
3997
+ out,
3998
+ "VSCode",
3999
+ "code --install-extension",
4000
+ name,
4001
+ at <= 0 ? void 0 : exactVersion(raw.slice(at + 1)),
4002
+ raw
4003
+ );
4004
+ }
4005
+ }
3525
4006
  function basename(w) {
3526
4007
  return (w ?? "").toLowerCase().split("/").pop() ?? "";
3527
4008
  }
@@ -3753,7 +4234,63 @@ function fromCall(words, out, depth) {
3753
4234
  pushPy(out, "pipenv", operands(words, v + 1, PIPENV_VALUE_FLAGS));
3754
4235
  return;
3755
4236
  }
4237
+ case "gem": {
4238
+ const v = verbIndex(words, start + 1, GEM_VALUE_FLAGS);
4239
+ if (v >= 0 && ["install", "i"].includes(lower(words[v]))) gemInstall(words, v + 1, out);
4240
+ return;
4241
+ }
4242
+ case "bundle": {
4243
+ const v = verbIndex(words, start + 1, BUNDLE_VALUE_FLAGS);
4244
+ if (v < 0 || lower(words[v]) !== "add") return;
4245
+ if (flagValues(words, v + 1, ["--git", "--github", "--path"]).length > 0) return;
4246
+ const version = exactVersion(flagValues(words, v + 1, ["-v", "--version"])[0]);
4247
+ for (const raw of operands(words, v + 1, BUNDLE_VALUE_FLAGS))
4248
+ if (GEM_NAME_RE.test(raw)) push(out, "RubyGems", "bundle add", raw, version, raw);
4249
+ return;
4250
+ }
4251
+ case "cargo": {
4252
+ const from = words[start + 1]?.startsWith("+") ? start + 2 : start + 1;
4253
+ const v = verbIndex(words, from, CARGO_ADD_VALUE_FLAGS);
4254
+ const verb = lower(words[v]);
4255
+ if (verb === "add") cargoCrates(words, v + 1, "cargo add", out);
4256
+ else if (verb === "install") cargoCrates(words, v + 1, "cargo install", out);
4257
+ return;
4258
+ }
4259
+ case "go": {
4260
+ const v = verbIndex(words, start + 1, GO_VALUE_FLAGS);
4261
+ const verb = lower(words[v]);
4262
+ if (verb === "get" || verb === "install" || verb === "run")
4263
+ goModules(words, v + 1, `go ${verb}`, out);
4264
+ return;
4265
+ }
4266
+ case "dotnet": {
4267
+ const v = verbIndex(words, start + 1, DOTNET_ADD_VALUE_FLAGS);
4268
+ const verb = lower(words[v]);
4269
+ if (verb === "add") dotnetAdd(words, v + 1, out);
4270
+ else if (verb === "tool" && ["install", "update"].includes(lower(words[v + 1])))
4271
+ dotnetTool(words, v + 2, out);
4272
+ return;
4273
+ }
4274
+ case "nuget": {
4275
+ if (lower(words[start + 1]) === "install") nugetInstall(words, start + 2, out);
4276
+ return;
4277
+ }
4278
+ case "mvn":
4279
+ case "mvnw":
4280
+ mavenGet(words, start + 1, out);
4281
+ return;
4282
+ case "composer": {
4283
+ let v = verbIndex(words, start + 1, COMPOSER_VALUE_FLAGS);
4284
+ if (v >= 0 && lower(words[v]) === "global") v = verbIndex(words, v + 1, COMPOSER_VALUE_FLAGS);
4285
+ const verb = lower(words[v]);
4286
+ if (verb === "require" || verb === "req" || verb === "r")
4287
+ composerPackages(words, v + 1, "composer require", out);
4288
+ else if (verb === "create-project")
4289
+ composerPackages(words, v + 1, "composer create-project", out);
4290
+ return;
4291
+ }
3756
4292
  default:
4293
+ if (EDITOR_HEADS.includes(family)) editorExtensions(words, start + 1, out);
3757
4294
  return;
3758
4295
  }
3759
4296
  }
@@ -5220,12 +5757,13 @@ function gitRepoPath(url) {
5220
5757
  function isDestructiveToolName(bareTool) {
5221
5758
  return DESTRUCTIVE_VERB_RE.test(bareTool);
5222
5759
  }
5223
- function* validCardRuns(text, base2) {
5760
+ function* validCardRuns(text, base2, exemptFractions = true) {
5224
5761
  const re = new RegExp(base2.source, "g");
5225
5762
  let m;
5226
5763
  while ((m = re.exec(text)) !== null) {
5227
5764
  const digits = m[0].replace(/\D/g, "");
5228
- if (!isDecimalFraction(text, m.index, digits.length === m[0].length) && validateLuhn(digits)) {
5765
+ const fraction = exemptFractions && isDecimalFraction(text, m.index, digits.length === m[0].length);
5766
+ if (!fraction && validateLuhn(digits)) {
5229
5767
  yield { index: m.index, length: m[0].length, digits };
5230
5768
  } else {
5231
5769
  re.lastIndex = m.index + 1;
@@ -5238,12 +5776,12 @@ function hasValidCard(text) {
5238
5776
  }
5239
5777
  return false;
5240
5778
  }
5241
- function replaceValidCards(text, replace) {
5779
+ function replaceValidCards(text, replace, { exemptFractions = true } = {}) {
5242
5780
  let out = text;
5243
5781
  for (const base2 of [PII_CC16_RE, PII_CC15_RE]) {
5244
5782
  let res = "";
5245
5783
  let last = 0;
5246
- for (const run of validCardRuns(out, base2)) {
5784
+ for (const run of validCardRuns(out, base2, exemptFractions)) {
5247
5785
  res += out.slice(last, run.index) + replace(run.digits);
5248
5786
  last = run.index + run.length;
5249
5787
  }
@@ -5257,11 +5795,11 @@ function isDecimalFraction(text, index, unseparated) {
5257
5795
  const a = text[index - 2];
5258
5796
  if (sep === ".") {
5259
5797
  if (a !== void 0 && a >= "0" && a <= "9") return true;
5260
- return a === void 0 || NUMBER_LEAD_RE.test(a);
5798
+ return a === void 0 || (a === "-" || a === "+") && index === 2;
5261
5799
  }
5262
5800
  if (sep === "," && a === "0") {
5263
5801
  const b = text[index - 3];
5264
- return b === void 0 || NUMBER_LEAD_RE.test(b);
5802
+ return b === void 0 || b === ";";
5265
5803
  }
5266
5804
  return false;
5267
5805
  }
@@ -5397,6 +5935,9 @@ function normalizeTrustedEntry(raw) {
5397
5935
  return full === null ? null : trustedEntryOf(full);
5398
5936
  }
5399
5937
  function maskPersonalData(text) {
5938
+ return maskText(text, false);
5939
+ }
5940
+ function maskText(text, exemptFractions) {
5400
5941
  if (!text) return text;
5401
5942
  try {
5402
5943
  let out = text;
@@ -5404,7 +5945,9 @@ function maskPersonalData(text) {
5404
5945
  const v = m.replace(/[ -]/g, "").toUpperCase();
5405
5946
  return validateIban(v) ? `${v.slice(0, 4)} **** **** ${v.slice(-4)}` : m;
5406
5947
  });
5407
- out = replaceValidCards(out, (digits) => `**** **** **** ${digits.slice(-4)}`);
5948
+ out = replaceValidCards(out, (digits) => `**** **** **** ${digits.slice(-4)}`, {
5949
+ exemptFractions
5950
+ });
5408
5951
  out = out.replace(
5409
5952
  ACCOUNT_VALUE_RE,
5410
5953
  (m, v) => IBAN_LENGTH[v.slice(0, 2).toUpperCase()] === v.length ? `"****${v.slice(-4)}"` : m
@@ -5427,7 +5970,7 @@ function maskLeaf(value, key, depth) {
5427
5970
  if (depth > MAX_MASK_DEPTH) return "[REDACTED]";
5428
5971
  if (typeof value === "string") {
5429
5972
  try {
5430
- const json = `{${JSON.stringify(key)}:${JSON.stringify(value)}}`;
5973
+ const json = `{${JSON.stringify(key)}:${JSON.stringify(maskPersonalData(value))}}`;
5431
5974
  return Object.values(JSON.parse(maskPersonalData(json)))[0];
5432
5975
  } catch {
5433
5976
  return "[REDACTED]";
@@ -5435,13 +5978,20 @@ function maskLeaf(value, key, depth) {
5435
5978
  }
5436
5979
  if (typeof value === "number") {
5437
5980
  const text = String(value);
5438
- const masked = maskPersonalData(text);
5981
+ const masked = maskText(text, true);
5439
5982
  return masked === text ? value : masked;
5440
5983
  }
5441
5984
  if (Array.isArray(value)) return value.map((v) => maskLeaf(v, key, depth + 1));
5442
5985
  if (value && typeof value === "object") {
5986
+ const used = /* @__PURE__ */ new Set();
5443
5987
  return Object.fromEntries(
5444
- Object.entries(value).map(([k, v]) => [maskPersonalData(k), maskLeaf(v, k, depth + 1)])
5988
+ Object.entries(value).map(([k, v]) => {
5989
+ const base2 = maskPersonalData(k);
5990
+ let name = base2;
5991
+ for (let n = 2; used.has(name); n++) name = `${base2}#${n}`;
5992
+ used.add(name);
5993
+ return [name, maskLeaf(v, k, depth + 1)];
5994
+ })
5445
5995
  );
5446
5996
  }
5447
5997
  return value;
@@ -5923,7 +6473,7 @@ function matchCanaryArgs(args, values) {
5923
6473
  return null;
5924
6474
  }
5925
6475
  }
5926
- var aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, 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;
6476
+ var aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, CHECK_TEXT, VERDICTS, CHECK_VERDICT_RANK, 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;
5927
6477
  var init_dist = __esm({
5928
6478
  "packages/policy-engine/dist/index.mjs"() {
5929
6479
  "use strict";
@@ -6724,6 +7274,11 @@ var init_dist = __esm({
6724
7274
  example: "cat /etc/shadow",
6725
7275
  advice: "Keep Review, or choose Block when agents must never read system password hashes."
6726
7276
  },
7277
+ "files.system-dirs": {
7278
+ 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.",
7279
+ example: "wget -O /usr/bin/tool https://example.com/tool",
7280
+ advice: "Keep Review on a workstation. A container image build that installs into /usr can set it to Log."
7281
+ },
6727
7282
  // ── Commands ──────────────────────────────────────────────────────────────
6728
7283
  "commands.inline-exec": {
6729
7284
  plain: "The agent runs a small program written inside the command itself.",
@@ -6920,7 +7475,7 @@ var init_dist = __esm({
6920
7475
  advice: "Fixed at Block. A person accepts the change on the machine (node9 mcp pin update)."
6921
7476
  },
6922
7477
  "loading.malicious-package": {
6923
- plain: "The agent installs a package that the public malicious-package database (OSV) lists, or one published minutes ago.",
7478
+ 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.",
6924
7479
  example: "npm install of a package flagged as malware",
6925
7480
  advice: "Keep Block for known-malicious packages; brand-new packages already stop for Review."
6926
7481
  }
@@ -6958,6 +7513,15 @@ var init_dist = __esm({
6958
7513
  defaultValue: "review",
6959
7514
  legacy: ["review-password-hash-read"]
6960
7515
  },
7516
+ {
7517
+ id: "files.system-dirs",
7518
+ group: "files",
7519
+ title: "System directories",
7520
+ catches: "Writes, new files and removals under /usr, /etc, /boot, /bin, /sbin, /lib and /var/log, and uploads of system files",
7521
+ values: ALL,
7522
+ defaultValue: "review",
7523
+ legacy: ["review-system-dir-mutation", "review-system-file-upload"]
7524
+ },
6961
7525
  // ── Commands ────────────────────────────────────────────────────────────
6962
7526
  {
6963
7527
  id: "commands.inline-exec",
@@ -8255,6 +8819,10 @@ var init_dist = __esm({
8255
8819
  AND_OP = probeBinOp("a && b");
8256
8820
  OR_OP = probeBinOp("a || b");
8257
8821
  AND_OR_OPS = new Set([AND_OP, OR_OP].filter((o) => o !== null));
8822
+ MAX_CALL_DEPTH = 4;
8823
+ MAX_CALL_WALKS = 256;
8824
+ callDepth = 0;
8825
+ callWalks = 0;
8258
8826
  FS_OP_CACHE_MAX = 5e3;
8259
8827
  fsOpCache = /* @__PURE__ */ new Map();
8260
8828
  stripDotSlash = (p) => p.replace(/^\.\//, "");
@@ -8307,10 +8875,79 @@ var init_dist = __esm({
8307
8875
  MAX_ARGV_CHARACTERS = 262144;
8308
8876
  ({ syntax: syntax2 } = mvdanSh2);
8309
8877
  MAX_ALTERNATIVES = 64;
8878
+ MAX_CALL_DEPTH2 = 4;
8879
+ MAX_CALL_WALKS2 = 256;
8880
+ POSITIONAL = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "@", "*", "#"];
8310
8881
  MAX_DEPTH = 64;
8311
8882
  MAX_PAYLOAD_DEPTH = 24;
8312
8883
  unique = (a) => [...new Set(a)].slice(0, MAX_ALTERNATIVES);
8313
- clone = (s) => ({ cwd: [...s.cwd], vars: new Map(s.vars) });
8884
+ clone = (s) => ({
8885
+ cwd: [...s.cwd],
8886
+ vars: new Map(s.vars),
8887
+ ...s.callDepth ? { callDepth: s.callDepth } : {}
8888
+ });
8889
+ SENDING_STDIN = /* @__PURE__ */ new Set(["nc", "ncat", "netcat", "socat", "telnet", "curl", "wget"]);
8890
+ DOWNLOAD_SHORT_VALUE = {
8891
+ curl: "odDeEFHKmPrtTuUwxXyYzAbcCQ",
8892
+ wget: "OoaPiBetTwQlARDXIUY",
8893
+ aria2c: "odisx"
8894
+ };
8895
+ DOWNLOAD_LONG_VALUE = {
8896
+ curl: [
8897
+ "--output",
8898
+ "--output-dir",
8899
+ "--dump-header",
8900
+ "--cookie-jar",
8901
+ "--data",
8902
+ "--data-binary",
8903
+ "--data-raw",
8904
+ "--data-ascii",
8905
+ "--data-urlencode",
8906
+ "--form",
8907
+ "--header",
8908
+ "--request",
8909
+ "--user",
8910
+ "--user-agent",
8911
+ "--referer",
8912
+ "--cookie",
8913
+ "--config",
8914
+ "--json",
8915
+ "--upload-file",
8916
+ "--max-time",
8917
+ "--proxy",
8918
+ "--write-out",
8919
+ "--url"
8920
+ ],
8921
+ wget: [
8922
+ "--output-document",
8923
+ "--output-file",
8924
+ "--append-output",
8925
+ "--directory-prefix",
8926
+ "--input-file",
8927
+ "--post-data",
8928
+ "--post-file",
8929
+ "--body-data",
8930
+ "--body-file",
8931
+ "--method",
8932
+ "--header",
8933
+ "--user-agent",
8934
+ "--user",
8935
+ "--password",
8936
+ "--tries",
8937
+ "--timeout"
8938
+ ],
8939
+ aria2c: ["--out", "--dir", "--input-file"]
8940
+ };
8941
+ DOWNLOAD_OUTPUT_FLAGS = {
8942
+ curl: ["-o", "--output", "-D", "--dump-header", "-c", "--cookie-jar"],
8943
+ wget: ["-O", "--output-document", "-o", "--output-file", "-a", "--append-output"],
8944
+ aria2c: ["-o", "--out"]
8945
+ };
8946
+ DOWNLOAD_DIR_FLAGS = {
8947
+ curl: ["--output-dir"],
8948
+ wget: ["-P", "--directory-prefix"],
8949
+ aria2c: ["-d", "--dir"]
8950
+ };
8314
8951
  base = (s) => (s ?? "").replace(/\/+$/, "").split("/").pop() ?? "";
8315
8952
  quoteGlob = (s) => s.replace(/[\\*?\[\]{}()!+@]/g, "\\$&");
8316
8953
  asWord = (p) => ({ value: p.value, ...p.glob ? { pattern: p.pattern } : {} });
@@ -8335,6 +8972,18 @@ var init_dist = __esm({
8335
8972
  SYSTEM_AUTH_DIRS = ["/etc/security", "/etc/pam.d", "/etc/sudoers.d"];
8336
8973
  PASSWORD_HASH_FILES = ["/etc/shadow", "/etc/gshadow"];
8337
8974
  STARTUP_NAMES = [".bashrc", ".zshrc", ".profile", ".bash_profile"];
8975
+ SYSTEM_DIRS = [
8976
+ "/usr",
8977
+ "/etc",
8978
+ "/boot",
8979
+ "/bin",
8980
+ "/sbin",
8981
+ "/lib",
8982
+ "/lib64",
8983
+ "/var/log"
8984
+ ];
8985
+ SYSTEM_DIRS_EXEMPT = ["/usr/local", "/usr/src"];
8986
+ UPLOAD_SYSTEM_DIRS = [...SYSTEM_DIRS, "/proc", "/sys", "/root"];
8338
8987
  AGENT_INSTRUCTION_FILES = [
8339
8988
  ".gitlab-ci.yml",
8340
8989
  "CLAUDE.md",
@@ -8352,6 +9001,7 @@ var init_dist = __esm({
8352
9001
  ".husky"
8353
9002
  ];
8354
9003
  LEGACY_SYSTEM_PATH_RE = /^(?:[a-zA-Z]:)?\/etc\/(?:passwd|shadow|sudoers)$/;
9004
+ exempt = (p) => SYSTEM_DIRS_EXEMPT.some((d) => within(p, d));
8355
9005
  RANK = { allow: 0, review: 1, block: 2 };
8356
9006
  BIP39_ENGLISH = `abandon
8357
9007
  ability
@@ -10637,15 +11287,20 @@ zoo`.split("\n");
10637
11287
  IDENTIFIER_VALUE_RE = /^[A-Za-z_.-]+$/;
10638
11288
  PATH_VALUE_RE = /^(?:\/|\.{1,2}\/|~\/)/;
10639
11289
  DOTTED_REFERENCE_RE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/;
11290
+ CONCATENATION_RE = /^\+[A-Za-z_$][\w$]*\./;
10640
11291
  EMAIL_VALUE_RE = /^[^@\s]+@[^@\s]+\.[A-Za-z]{2,}$/;
10641
11292
  FIXTURE_NAME_RE = /(?:fake|dummy|example|sample|mock|stub|placeholder|test)/i;
10642
- 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;
11293
+ 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;
10643
11294
  QUERY_CREDENTIAL_VALUE_RE = /=([A-Za-z0-9%+/_.~-]{8,512}={0,2})$/;
10644
11295
  OTP_SECRET_RE = /^[A-Z2-7]{16,}=*$/i;
10645
- ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
11296
+ OTP_DOC_SECRETS = /* @__PURE__ */ new Set(["JBSWY3DPEHPK3PXP", "GEZDGNBVGY3TQOJQ"]);
11297
+ ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/_.!@#$%^*~-]{12,512}={0,2})$/;
10646
11298
  ASSIGNED_SECRET_NAME_RE = /^([A-Za-z0-9_]+)/;
10647
- NAME_KEYWORD_RE = /^(?:password|passphrase|passwd|pwd|secret|token|api[_-]?key|auth[_-]?key|private[_-]?key|access[_-]?key|client[_-]?secret)/i;
11299
+ 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;
10648
11300
  ARN_CONTEXT_RE = /\barn:[A-Za-z0-9-]*:[^\s"'`]*$/i;
11301
+ BASIC_VALUE_RE = /([A-Za-z0-9+/]{8,512}={0,2})$/;
11302
+ BASIC_PAIR_RE = /^([^:\s]{1,128}):(\S{8,256})$/;
11303
+ OTP_CAPTURE_RE = /["']([A-Z2-7]{16,128}=*)["']$/i;
10649
11304
  DLP_PATTERNS = [
10650
11305
  // ── AWS ───────────────────────────────────────────────────────────────────
10651
11306
  {
@@ -10983,6 +11638,22 @@ zoo`.split("\n");
10983
11638
  contextBoost: true,
10984
11639
  minEntropy: 3
10985
11640
  },
11641
+ // ── HTTP Basic credential ─────────────────────────────────────────────────
11642
+ // `Authorization: Basic <base64 of user:password>`, or the bare scheme in a
11643
+ // config list. The validator decodes the value and accepts only a
11644
+ // `user:password` whose password looks generated (letters and digits, 8+
11645
+ // characters, the query-credential rule), so prose ("Basic authentication")
11646
+ // and documentation pairs (`admin:changeme`, `user:pass1234`) stay allowed.
11647
+ // review: the same weight as a bare Bearer token.
11648
+ {
11649
+ name: "HTTP Basic Credential",
11650
+ regex: /\bBasic[ \t]{1,8}([A-Za-z0-9+/]{8,512}={0,2})/i,
11651
+ severity: "review",
11652
+ keywords: ["basic"],
11653
+ validate: validateBasicCredential,
11654
+ redactGroup: 1,
11655
+ argumentsOnly: true
11656
+ },
10986
11657
  // ── Resend ────────────────────────────────────────────────────────────────
10987
11658
  {
10988
11659
  name: "Resend API Key",
@@ -11124,6 +11795,19 @@ zoo`.split("\n");
11124
11795
  validate: validateQueryCredential,
11125
11796
  argumentsOnly: true
11126
11797
  },
11798
+ // ── OTP secret in a constructor ───────────────────────────────────────────
11799
+ // `ROTP::TOTP.new('<base32>')`, `pyotp.TOTP("<base32>")`, `new HOTP('...')`:
11800
+ // the shared secret of a one-time-password generator written as a literal.
11801
+ // The otpauth URI form is `Credential in URL Query`. review.
11802
+ {
11803
+ name: "OTP Secret",
11804
+ regex: /\b(?:TOTP|HOTP|OTP)(?:\.new|\.create)?[ \t]{0,8}\(?[ \t]{0,8}["']([A-Z2-7]{16,128}=*)["']/i,
11805
+ severity: "review",
11806
+ keywords: ["otp"],
11807
+ validate: validateOtpSecret,
11808
+ redactGroup: 1,
11809
+ argumentsOnly: true
11810
+ },
11127
11811
  // ── Secret assigned to a telling name ─────────────────────────────────────
11128
11812
  // `password: ...`, `JWT_SECRET=...`, `api_key = "..."`. The name alone is
11129
11813
  // not a finding: most such lines hold a reference (`process.env.X`), a
@@ -11131,12 +11815,14 @@ zoo`.split("\n");
11131
11815
  // generated: 12+ characters, not an identifier, Shannon entropy of 3.5
11132
11816
  // bits per character or more. review, never block (node9Firewall#348,
11133
11817
  // item 4). Tuned on CredData's public split; its look-alike lines are the
11134
- // negative control.
11818
+ // negative control. A quote may be escaped (`\"access_token\":\"...\"`):
11819
+ // JSON written inside a string literal. The two optional quotes after the
11820
+ // name are written out, not as a counted group, to keep star height 1.
11135
11821
  {
11136
11822
  name: "Assigned Secret",
11137
- 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,
11823
+ 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,
11138
11824
  severity: "review",
11139
- keywords: ["password", "passphrase", "passwd", "pwd", "secret", "token", "key"],
11825
+ keywords: ["pass", "pwd", "secret", "token", "key", "ticket"],
11140
11826
  validate: validateAssignedSecret,
11141
11827
  argumentsOnly: true
11142
11828
  }
@@ -11397,7 +12083,169 @@ zoo`.split("\n");
11397
12083
  ]);
11398
12084
  PY_SPEC_RE = /^([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)(?:\[[^\]]*\])?\s*(.*)$/;
11399
12085
  PY_EXACT_RE = /^===?\s*v?(\d+(?:\.\d+)*(?:[a-zA-Z0-9.+!-]*))$/;
12086
+ EXACT_VERSION_RE = /^=?v?(\d+(?:\.\d+)*(?:[-+][0-9A-Za-z][0-9A-Za-z.+-]*)?)$/;
12087
+ GEM_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
12088
+ GEM_VALUE_FLAGS = /* @__PURE__ */ new Set([
12089
+ "-v",
12090
+ "--version",
12091
+ "-i",
12092
+ "--install-dir",
12093
+ "-n",
12094
+ "--bindir",
12095
+ "--document",
12096
+ "--build-root",
12097
+ "-P",
12098
+ "--trust-policy",
12099
+ "-s",
12100
+ "--source",
12101
+ "-p",
12102
+ "--http-proxy",
12103
+ "--platform",
12104
+ "-g",
12105
+ "--file",
12106
+ "--without"
12107
+ ]);
12108
+ BUNDLE_VALUE_FLAGS = /* @__PURE__ */ new Set([
12109
+ "-v",
12110
+ "--version",
12111
+ "-g",
12112
+ "--group",
12113
+ "-s",
12114
+ "--source",
12115
+ "-r",
12116
+ "--require",
12117
+ "--path",
12118
+ "--git",
12119
+ "--github",
12120
+ "--branch",
12121
+ "--ref",
12122
+ "--glob"
12123
+ ]);
12124
+ CRATE_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_-]*$/u;
12125
+ CARGO_ADD_VALUE_FLAGS = /* @__PURE__ */ new Set([
12126
+ "-F",
12127
+ "--features",
12128
+ "--rename",
12129
+ "--registry",
12130
+ "--manifest-path",
12131
+ "-p",
12132
+ "--package",
12133
+ "--git",
12134
+ "--branch",
12135
+ "--tag",
12136
+ "--rev",
12137
+ "--path",
12138
+ "--base",
12139
+ "--target",
12140
+ "--color",
12141
+ "--config",
12142
+ "-Z",
12143
+ "--lockfile-path"
12144
+ ]);
12145
+ CARGO_INSTALL_VALUE_FLAGS = /* @__PURE__ */ new Set([
12146
+ "--version",
12147
+ "--vers",
12148
+ "--git",
12149
+ "--branch",
12150
+ "--tag",
12151
+ "--rev",
12152
+ "--path",
12153
+ "--root",
12154
+ "--index",
12155
+ "--registry",
12156
+ "-F",
12157
+ "--features",
12158
+ "--profile",
12159
+ "--target",
12160
+ "--target-dir",
12161
+ "-j",
12162
+ "--jobs",
12163
+ "--bin",
12164
+ "--example",
12165
+ "--color",
12166
+ "--config",
12167
+ "-Z"
12168
+ ]);
12169
+ GO_VALUE_FLAGS = /* @__PURE__ */ new Set([
12170
+ "-C",
12171
+ "-tags",
12172
+ "-ldflags",
12173
+ "-gcflags",
12174
+ "-asmflags",
12175
+ "-mod",
12176
+ "-modfile",
12177
+ "-o",
12178
+ "-p",
12179
+ "-pkgdir",
12180
+ "-toolexec",
12181
+ "-exec",
12182
+ "-overlay",
12183
+ "-pgo",
12184
+ "-buildmode",
12185
+ "-compiler",
12186
+ "-gccgoflags",
12187
+ "-installsuffix",
12188
+ "-covermode",
12189
+ "-coverpkg"
12190
+ ]);
12191
+ GO_PATH_RE = /^[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+(?:\/[\p{L}\p{N}_.~+-]+)*$/u;
12192
+ NUGET_NAME_RE = /^[\p{L}\p{N}_][\p{L}\p{N}_.-]*$/u;
12193
+ DOTNET_ADD_VALUE_FLAGS = /* @__PURE__ */ new Set([
12194
+ "-v",
12195
+ "--version",
12196
+ "-s",
12197
+ "--source",
12198
+ "-f",
12199
+ "--framework",
12200
+ "--package-directory"
12201
+ ]);
12202
+ DOTNET_TOOL_VALUE_FLAGS = /* @__PURE__ */ new Set([
12203
+ "--version",
12204
+ "--add-source",
12205
+ "--configfile",
12206
+ "--framework",
12207
+ "--tool-path",
12208
+ "-a",
12209
+ "--arch",
12210
+ "--tool-manifest",
12211
+ "-v",
12212
+ "--verbosity"
12213
+ ]);
12214
+ NUGET_VALUE_FLAGS = /* @__PURE__ */ new Set([
12215
+ "-version",
12216
+ "-outputdirectory",
12217
+ "-o",
12218
+ "-source",
12219
+ "-configfile",
12220
+ "-fallbacksource",
12221
+ "-framework",
12222
+ "-packagesavemode",
12223
+ "-dependencyversion",
12224
+ "-solutiondirectory",
12225
+ "-verbosity"
12226
+ ]);
12227
+ MAVEN_COORD_RE = /^[\p{L}\p{N}_.-]+:[\p{L}\p{N}_.-]+$/u;
12228
+ COMPOSER_NAME_RE = /^[\p{L}\p{N}][\p{L}\p{N}_.-]*\/[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
12229
+ COMPOSER_VALUE_FLAGS = /* @__PURE__ */ new Set([
12230
+ "-d",
12231
+ "--working-dir",
12232
+ "--stability",
12233
+ "--repository",
12234
+ "--add-repository"
12235
+ ]);
12236
+ EDITOR_HEADS = ["code", "code-insiders", "codium", "vscodium", "cursor"];
12237
+ EXTENSION_ID_RE = /^[\p{L}\p{N}][\p{L}\p{N}_-]*\.[\p{L}\p{N}][\p{L}\p{N}_.-]*$/u;
11400
12238
  MANAGER_HEADS = /* @__PURE__ */ new Set([
12239
+ "gem",
12240
+ "bundle",
12241
+ "cargo",
12242
+ "go",
12243
+ "dotnet",
12244
+ "nuget",
12245
+ "mvn",
12246
+ "mvnw",
12247
+ "composer",
12248
+ ...EDITOR_HEADS,
11401
12249
  "npm",
11402
12250
  "npx",
11403
12251
  "pnpm",
@@ -11476,6 +12324,7 @@ zoo`.split("\n");
11476
12324
  MAP_GOVERNED = /* @__PURE__ */ new Set([
11477
12325
  "files.shell-startup",
11478
12326
  "files.password-hashes",
12327
+ "files.system-dirs",
11479
12328
  "commands.inline-exec",
11480
12329
  "commands.eval-dynamic",
11481
12330
  "commands.curl-pipe-shell",
@@ -11744,7 +12593,6 @@ zoo`.split("\n");
11744
12593
  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/;
11745
12594
  PII_CC15_RE = /\b3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}\b/;
11746
12595
  PII_IBAN_RE = /\b[A-Z]{2}\d{2}(?:[A-Z0-9]|[ -][A-Z0-9]){11,30}\b/;
11747
- NUMBER_LEAD_RE = /[\s:=;([{"'+-]/;
11748
12596
  REALTIME_PII_PATTERNS = ["SSN", "Credit Card", "IBAN"];
11749
12597
  MAX_PII_SCAN_BYTES = 1e5;
11750
12598
  MASKED_KINDS = /* @__PURE__ */ new Set(["email", "iban", "account"]);
@@ -14599,14 +15447,23 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
14599
15447
  clearTimeout(timer);
14600
15448
  }
14601
15449
  }
14602
- async function waitForDaemonDecision(id, signal) {
15450
+ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
14603
15451
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
14604
15452
  const waitCtrl = new AbortController();
14605
- const waitTimer = setTimeout(() => waitCtrl.abort(), 12e4);
15453
+ let capped = false;
15454
+ const waitTimer = maxWaitMs > 0 ? setTimeout(() => {
15455
+ capped = true;
15456
+ waitCtrl.abort();
15457
+ }, maxWaitMs) : void 0;
14606
15458
  const onAbort = () => waitCtrl.abort();
14607
15459
  if (signal) signal.addEventListener("abort", onAbort);
14608
15460
  try {
14609
- const waitRes = await fetch(`${base2}/wait/${id}`, { signal: waitCtrl.signal });
15461
+ const waitRes = await fetch(`${base2}/wait/${id}`, { signal: waitCtrl.signal }).catch(
15462
+ (err) => {
15463
+ if (capped && !signal?.aborted) throw new Error("node9 daemon did not answer in time");
15464
+ throw err;
15465
+ }
15466
+ );
14610
15467
  if (!waitRes.ok) return { decision: "deny" };
14611
15468
  const { decision, source, reason } = await waitRes.json();
14612
15469
  if (decision === "allow") return { decision: "allow", source };
@@ -15647,13 +16504,13 @@ function maskRiskMetadataForCloud(rm) {
15647
16504
  const r = rm;
15648
16505
  const dests = Array.isArray(r.exposureDestinations) ? r.exposureDestinations.filter((d) => typeof d === "string") : [];
15649
16506
  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);
15650
- const maskText = (text) => {
16507
+ const maskText2 = (text) => {
15651
16508
  let out2 = text;
15652
16509
  for (const [full, masked] of swaps) out2 = out2.split(full).join(masked);
15653
16510
  return maskPersonalData(out2);
15654
16511
  };
15655
16512
  const out = {};
15656
- for (const [k, v] of Object.entries(r)) out[k] = typeof v === "string" ? maskText(v) : v;
16513
+ for (const [k, v] of Object.entries(r)) out[k] = typeof v === "string" ? maskText2(v) : v;
15657
16514
  if (dests.length > 0) {
15658
16515
  out.exposureDestinations = dests.map(maskDestination);
15659
16516
  out.exposureDestinationHashes = dests.map((d) => isMaskedKind(d) ? hashDestination(d) : "");
@@ -15856,7 +16713,17 @@ function ecoDir(eco) {
15856
16713
  return path23.join(osvRoot(), eco);
15857
16714
  }
15858
16715
  function indexKey(eco, name) {
15859
- return eco === "PyPI" ? normalizePyPiName(name) : name.toLowerCase();
16716
+ switch (eco) {
16717
+ case "PyPI":
16718
+ return normalizePyPiName(name);
16719
+ case "crates.io":
16720
+ return name.toLowerCase().replace(/_/g, "-");
16721
+ case "Go":
16722
+ case "Maven":
16723
+ return name;
16724
+ default:
16725
+ return name.toLowerCase();
16726
+ }
15860
16727
  }
15861
16728
  function shardOf(key) {
15862
16729
  return createHash4("sha1").update(key).digest("hex").slice(0, 2);
@@ -16067,8 +16934,11 @@ async function pypiInfo(name, version) {
16067
16934
  publishedAtMs: times.length > 0 ? Math.min(...times) : void 0
16068
16935
  };
16069
16936
  }
16937
+ var HAS_REGISTRY_CLIENT = /* @__PURE__ */ new Set(["npm", "PyPI"]);
16070
16938
  async function registryInfo(eco, name, version, freshWindowMs, raw) {
16071
- return eco === "npm" ? npmInfo(name, version, freshWindowMs, raw) : pypiInfo(name, version);
16939
+ if (eco === "npm") return npmInfo(name, version, freshWindowMs, raw);
16940
+ if (eco === "PyPI") return pypiInfo(name, version);
16941
+ return null;
16072
16942
  }
16073
16943
 
16074
16944
  // src/supply-chain/local-resolve.ts
@@ -16107,26 +16977,51 @@ function commandOf(toolName, args) {
16107
16977
  const a = args;
16108
16978
  return typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : null;
16109
16979
  }
16980
+ var KIND = {
16981
+ npm: "npm package",
16982
+ PyPI: "PyPI package",
16983
+ RubyGems: "RubyGems package",
16984
+ "crates.io": "crates.io package",
16985
+ Go: "Go module",
16986
+ NuGet: "NuGet package",
16987
+ Maven: "Maven artifact",
16988
+ Packagist: "Packagist package",
16989
+ VSCode: "VS Code extension"
16990
+ };
16110
16991
  function label(p, version) {
16111
- return `${p.ecosystem === "npm" ? "npm" : "PyPI"} package "${p.name}${version ? `@${version}` : ""}"`;
16992
+ return `${KIND[p.ecosystem]} "${p.name}${version ? `@${version}` : ""}"`;
16993
+ }
16994
+ function osvNames(p) {
16995
+ if (p.ecosystem !== "Go") return [p.name];
16996
+ const parts = p.name.split("/");
16997
+ const out = [];
16998
+ for (let n = parts.length; n >= 2 && out.length < 6; n--) out.push(parts.slice(0, n).join("/"));
16999
+ return out;
16112
17000
  }
16113
17001
  async function maliciousFor(p, version, cfg, misses, networkCapped) {
16114
- const local = lookupIndex(p.ecosystem, p.name);
16115
- const entries = local.status === "hit" ? local.entries : null;
16116
- if (!entries && !(local.status === "clean" && local.fresh)) {
17002
+ const names = osvNames(p);
17003
+ const local = names.map((n) => lookupIndex(p.ecosystem, n));
17004
+ const hits2 = local.flatMap((l) => l.status === "hit" ? l.entries : []);
17005
+ const entries = hits2.length > 0 ? hits2 : null;
17006
+ const freshClean = local.every((l) => l.status === "clean" && l.fresh);
17007
+ const status = local.some((l) => l.status === "unavailable") ? "unavailable" : local[0].status;
17008
+ if (!entries && !freshClean) {
16117
17009
  if (!cfg.onlineFallback || networkCapped) {
16118
17010
  misses.push(
16119
- `${label(p, version)}: local index ${local.status}, no online lookup (${networkCapped ? "over the per-command network cap" : "online fallback off"})`
17011
+ `${label(p, version)}: local index ${status}, no online lookup (${networkCapped ? "over the per-command network cap" : "online fallback off"})`
16120
17012
  );
16121
17013
  return null;
16122
17014
  }
16123
- let ids = null;
17015
+ let perName = null;
16124
17016
  try {
16125
- ids = await queryOsvMalicious([{ ecosystem: p.ecosystem, name: p.name, version }]);
17017
+ perName = await queryOsvMalicious(
17018
+ names.map((name) => ({ ecosystem: p.ecosystem, name, version }))
17019
+ );
16126
17020
  } catch (err) {
16127
17021
  misses.push(`${label(p, version)}: OSV online ${err.message}`);
16128
17022
  return null;
16129
17023
  }
17024
+ const ids = perName ? [[...new Set(perName.flat())]] : null;
16130
17025
  if (!ids) {
16131
17026
  misses.push(`${label(p, version)}: OSV online unavailable`);
16132
17027
  return null;
@@ -16196,7 +17091,7 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
16196
17091
  }
16197
17092
  const findings = [];
16198
17093
  let info = null;
16199
- const wantsRegistry = cfg.newPackage !== "off" || cfg.installScript !== "off";
17094
+ const wantsRegistry = (cfg.newPackage !== "off" || cfg.installScript !== "off") && HAS_REGISTRY_CLIENT.has(p.ecosystem);
16200
17095
  if (wantsRegistry && !networkCapped) {
16201
17096
  try {
16202
17097
  const windowMs = cfg.newPackage === "off" ? 0 : cfg.maxAgeHours * 36e5;
@@ -16277,6 +17172,36 @@ async function runPackageCheck(toolName, args, cfg, cwd) {
16277
17172
  }
16278
17173
  }
16279
17174
 
17175
+ // src/policy/negotiation.ts
17176
+ var BLOCKED_BY = " is blocked by ";
17177
+ var DASH_BLOCKED_BY = " \u2014 blocked by ";
17178
+ var SHIELD = " shield";
17179
+ function asApprovalRequest(description) {
17180
+ const is = description.lastIndexOf(BLOCKED_BY);
17181
+ if (is > 0) {
17182
+ const by = description.slice(is + BLOCKED_BY.length).replace(/\.$/, "");
17183
+ if (by.endsWith(SHIELD)) return `${description.slice(0, is)} needs your approval (${by})`;
17184
+ }
17185
+ const dash = description.lastIndexOf(DASH_BLOCKED_BY);
17186
+ if (dash > 0) {
17187
+ const by = description.slice(dash + DASH_BLOCKED_BY.length).replace(/\.$/, "");
17188
+ if (by.endsWith(SHIELD)) return `${description.slice(0, dash)}. Needs your approval (${by})`;
17189
+ }
17190
+ return description;
17191
+ }
17192
+
17193
+ // src/auth/decision-sources.ts
17194
+ var HUMAN_DECISION_SOURCES = /* @__PURE__ */ new Set([
17195
+ "terminal",
17196
+ "browser",
17197
+ "native"
17198
+ ]);
17199
+ var REDIRECT_SOURCE = "terminal-redirect";
17200
+ var DECISION_ROUTE_SOURCES = /* @__PURE__ */ new Set([
17201
+ ...HUMAN_DECISION_SOURCES,
17202
+ REDIRECT_SOURCE
17203
+ ]);
17204
+
16280
17205
  // src/auth/orchestrator.ts
16281
17206
  var SENSITIVE_PATH_PATTERN = sensitivePathMatch("").patternName;
16282
17207
  function isProtectedPathRule(ruleName) {
@@ -16404,6 +17329,10 @@ async function authorizeHeadless(toolName, args, meta, options) {
16404
17329
  }
16405
17330
  return _authorizeHeadlessCore(toolName, args, meta, options);
16406
17331
  }
17332
+ function daemonWaitCapMs(approvalTimeoutMs) {
17333
+ if (approvalTimeoutMs === 0) return 0;
17334
+ return Math.max(approvalTimeoutMs ?? 0, 12e4) + 5e3;
17335
+ }
16407
17336
  function approvalTimeoutReason(approvalTimeoutMs) {
16408
17337
  return `No human response within ${approvalTimeoutMs / 1e3}s \u2014 auto-denied by timeout policy. Approve from your phone next time: run \`node9 login\`.`;
16409
17338
  }
@@ -16439,6 +17368,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
16439
17368
  let statefulRecoveryCommand;
16440
17369
  let localSmartRuleMatched = false;
16441
17370
  let hardBlockDowngraded = false;
17371
+ let downgradedBlock = null;
16442
17372
  let argScan;
16443
17373
  const argumentScan = () => argScan ??= scanArgumentSecrets(args);
16444
17374
  let fileAccess;
@@ -17058,6 +17988,16 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
17058
17988
  humanApproverReachable,
17059
17989
  overridable: policyResult.overridable
17060
17990
  });
17991
+ const blocked = {
17992
+ approved: false,
17993
+ reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
17994
+ blockedBy: "local-config",
17995
+ blockedByLabel: policyResult.blockedByLabel,
17996
+ ruleHit: policyResult.ruleName,
17997
+ ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
17998
+ ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription },
17999
+ ...protectedPathFields(policyResult.ruleName, toolName, args, dlpReviewFlagged)
18000
+ };
17061
18001
  const hardBlock = () => {
17062
18002
  if (!isManual)
17063
18003
  appendLocalAudit(
@@ -17068,17 +18008,9 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
17068
18008
  { ...meta, ruleName: policyResult.ruleName },
17069
18009
  hashAuditArgs
17070
18010
  );
17071
- return {
17072
- approved: false,
17073
- reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
17074
- blockedBy: "local-config",
17075
- blockedByLabel: policyResult.blockedByLabel,
17076
- ruleHit: policyResult.ruleName,
17077
- ...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
17078
- ...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription },
17079
- ...protectedPathFields(policyResult.ruleName, toolName, args, dlpReviewFlagged)
17080
- };
18011
+ return blocked;
17081
18012
  };
18013
+ downgradedBlock = blocked;
17082
18014
  if (policyResult.dependsOnStatePredicates?.length) {
17083
18015
  const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
17084
18016
  const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
@@ -17135,7 +18067,8 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
17135
18067
  policyMatchedWord,
17136
18068
  policyResult.ruleName
17137
18069
  );
17138
- if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
18070
+ if (policyRuleDescription)
18071
+ riskMetadata.ruleDescription = (hardBlockDowngraded ? asApprovalRequest(policyRuleDescription) : policyRuleDescription).slice(0, 200);
17139
18072
  const persistent = policyResult.ruleName || policyResult.tier === 7 || hardBlockDowngraded ? null : getPersistentDecision(toolName);
17140
18073
  if (persistent === "allow" && !callSpecificReview()) {
17141
18074
  if (!isManual)
@@ -17299,7 +18232,7 @@ ${packageReview}`;
17299
18232
  }
17300
18233
  }
17301
18234
  if (initResult.pending) cloudRequestId = initResult.requestId || null;
17302
- if (!callSpecificReview()) explainableLabel = "Organization Policy (SaaS)";
18235
+ if (!reviewDemanded()) explainableLabel = "Organization Policy (SaaS)";
17303
18236
  } catch {
17304
18237
  }
17305
18238
  }
@@ -17441,10 +18374,54 @@ ${packageReview}`;
17441
18374
  decision: daemonDecision,
17442
18375
  source: decisionSource,
17443
18376
  reason: daemonReason
17444
- } = await waitForDaemonDecision(daemonEntryId, signal);
18377
+ } = await waitForDaemonDecision(
18378
+ daemonEntryId,
18379
+ signal,
18380
+ daemonWaitCapMs(config.settings.approvalTimeoutMs)
18381
+ );
17445
18382
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
17446
18383
  const isApproved = daemonDecision === "allow";
17447
- const isRedirect = decisionSource === "terminal-redirect";
18384
+ const isRedirect = decisionSource === REDIRECT_SOURCE;
18385
+ if (!isApproved && !isRedirect && !(decisionSource && HUMAN_DECISION_SOURCES.has(decisionSource))) {
18386
+ if (decisionSource === "cloud")
18387
+ return {
18388
+ approved: false,
18389
+ reason: daemonReason || "Action rejected by organization administrator via Slack.",
18390
+ blockedBy: "team-policy",
18391
+ blockedByLabel: "Organization Policy (SaaS)",
18392
+ decisionSource: "cloud"
18393
+ };
18394
+ const timedOut = decisionSource === "timeout";
18395
+ if (downgradedBlock)
18396
+ return {
18397
+ ...downgradedBlock,
18398
+ blockedBy: timedOut ? "timeout" : "local-config"
18399
+ };
18400
+ if (timedOut)
18401
+ return {
18402
+ approved: false,
18403
+ reason: daemonReason || "No response: auto-denied after the approval timeout.",
18404
+ blockedBy: "timeout",
18405
+ blockedByLabel: "Approval Timeout"
18406
+ };
18407
+ if (decisionSource === "policy")
18408
+ return {
18409
+ approved: false,
18410
+ reason: daemonReason || "Action blocked by policy.",
18411
+ blockedBy: "local-config",
18412
+ blockedByLabel: explainableLabel
18413
+ };
18414
+ return {
18415
+ approved: false,
18416
+ reason: daemonReason || "node9 could not get an approval decision for this call.",
18417
+ blockedBy: "no-approval-mechanism",
18418
+ blockedByLabel: "Approval Unavailable"
18419
+ };
18420
+ }
18421
+ if (isApproved && decisionSource === "cloud")
18422
+ return { approved: true, checkedBy: "cloud", decisionSource: "cloud" };
18423
+ if (isApproved && decisionSource === "policy")
18424
+ return { approved: true, checkedBy: "local-policy" };
17448
18425
  const via = "Terminal (node9 tail)";
17449
18426
  return {
17450
18427
  approved: isApproved,