@node9/proxy 2.8.5 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +3771 -1865
- package/dist/cli.mjs +3707 -1801
- package/dist/dashboard.mjs +854 -165
- package/dist/index.js +968 -113
- package/dist/index.mjs +964 -109
- package/dist/scan-ink.mjs +213 -99
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -152,13 +152,21 @@ function filePathFromArgs(args) {
|
|
|
152
152
|
return void 0;
|
|
153
153
|
}
|
|
154
154
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
155
|
-
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern) || /dlp|taint/i.test(String(meta?.ruleName ?? ""));
|
|
155
|
+
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || checkedBy.toLowerCase().includes("pii") || Boolean(meta?.dlpPattern) || Boolean(meta?.piiPatterns) || Boolean(meta?.canaryId) || /dlp|taint/i.test(String(meta?.ruleName ?? ""));
|
|
156
156
|
const preview = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
|
|
157
157
|
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args), ...preview ? { argsPreview: preview } : {} } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
158
158
|
const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
|
|
159
159
|
const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
|
|
160
160
|
const agentToolNameField = meta?.agentToolName ? { agentToolName: meta.agentToolName } : {};
|
|
161
161
|
const dlpFields = meta?.dlpPattern ? { dlpPattern: meta.dlpPattern, dlpSample: meta.dlpSample } : {};
|
|
162
|
+
const canaryFields = meta?.canaryId ? {
|
|
163
|
+
canaryId: meta.canaryId,
|
|
164
|
+
...meta.canaryHash && { canaryHash: meta.canaryHash },
|
|
165
|
+
...meta.canaryKind && { canaryKind: meta.canaryKind },
|
|
166
|
+
...meta.canaryPath && { canaryPath: meta.canaryPath },
|
|
167
|
+
...meta.canaryView && { canaryView: meta.canaryView },
|
|
168
|
+
...meta.canaryRetired && { canaryRetired: true }
|
|
169
|
+
} : {};
|
|
162
170
|
const cloudLinkField = meta?.cloudRequestId ? { cloudRequestId: meta.cloudRequestId } : {};
|
|
163
171
|
const workingDirField = meta?.workingDir ? { workingDir: meta.workingDir } : {};
|
|
164
172
|
const shell = process.env.SHELL ? import_path.default.basename(process.env.SHELL) : void 0;
|
|
@@ -184,6 +192,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
184
192
|
checkedBy,
|
|
185
193
|
...ruleNameField,
|
|
186
194
|
...dlpFields,
|
|
195
|
+
...canaryFields,
|
|
187
196
|
...cloudLinkField,
|
|
188
197
|
...workingDirField,
|
|
189
198
|
...shellTypeField,
|
|
@@ -372,7 +381,11 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
372
381
|
mode: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
373
382
|
allow: import_zod.z.array(import_zod.z.string()).optional(),
|
|
374
383
|
deny: import_zod.z.array(import_zod.z.string()).optional(),
|
|
375
|
-
allowPrivate: import_zod.z.boolean().optional()
|
|
384
|
+
allowPrivate: import_zod.z.boolean().optional(),
|
|
385
|
+
// SSRF floor. `ssrfAllow` exempts OVERRIDABLE tiers only; a tier-1
|
|
386
|
+
// entry is dropped with a warning at load, never silently honoured.
|
|
387
|
+
ssrfAllow: import_zod.z.array(import_zod.z.string()).optional(),
|
|
388
|
+
ssrfStrict: import_zod.z.boolean().optional()
|
|
376
389
|
}).optional(),
|
|
377
390
|
loopDetection: import_zod.z.object({
|
|
378
391
|
enabled: import_zod.z.boolean().optional(),
|
|
@@ -409,8 +422,8 @@ function sanitizeConfig(raw) {
|
|
|
409
422
|
}
|
|
410
423
|
}
|
|
411
424
|
const lines = result.error.issues.map((issue) => {
|
|
412
|
-
const
|
|
413
|
-
return ` \u2022 ${
|
|
425
|
+
const path15 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
426
|
+
return ` \u2022 ${path15}: ${issue.message}`;
|
|
414
427
|
});
|
|
415
428
|
return {
|
|
416
429
|
sanitized,
|
|
@@ -426,11 +439,178 @@ var import_os2 = __toESM(require("os"));
|
|
|
426
439
|
|
|
427
440
|
// packages/policy-engine/dist/index.mjs
|
|
428
441
|
var import_safe_regex2 = __toESM(require("safe-regex2"), 1);
|
|
442
|
+
var import_crypto3 = require("crypto");
|
|
429
443
|
var import_mvdan_sh = __toESM(require("mvdan-sh"), 1);
|
|
430
444
|
var import_picomatch = __toESM(require("picomatch"), 1);
|
|
431
445
|
var import_safe_regex22 = __toESM(require("safe-regex2"), 1);
|
|
432
446
|
var import_safe_regex23 = __toESM(require("safe-regex2"), 1);
|
|
433
|
-
var
|
|
447
|
+
var import_crypto4 = __toESM(require("crypto"), 1);
|
|
448
|
+
function validateLuhn(digits) {
|
|
449
|
+
if (!/^\d+$/.test(digits)) return false;
|
|
450
|
+
if (digits.length < 12) return false;
|
|
451
|
+
if (!/[1-9]/.test(digits)) return false;
|
|
452
|
+
let sum = 0;
|
|
453
|
+
let double = false;
|
|
454
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
455
|
+
let d = digits.charCodeAt(i) - 48;
|
|
456
|
+
if (double) {
|
|
457
|
+
d *= 2;
|
|
458
|
+
if (d > 9) d -= 9;
|
|
459
|
+
}
|
|
460
|
+
sum += d;
|
|
461
|
+
double = !double;
|
|
462
|
+
}
|
|
463
|
+
return sum % 10 === 0;
|
|
464
|
+
}
|
|
465
|
+
var IBAN_LENGTH = {
|
|
466
|
+
AD: 24,
|
|
467
|
+
AE: 23,
|
|
468
|
+
AL: 28,
|
|
469
|
+
AT: 20,
|
|
470
|
+
AZ: 28,
|
|
471
|
+
BA: 20,
|
|
472
|
+
BE: 16,
|
|
473
|
+
BG: 22,
|
|
474
|
+
BH: 22,
|
|
475
|
+
BI: 27,
|
|
476
|
+
BR: 29,
|
|
477
|
+
BY: 28,
|
|
478
|
+
CH: 21,
|
|
479
|
+
CR: 22,
|
|
480
|
+
CY: 28,
|
|
481
|
+
CZ: 24,
|
|
482
|
+
DE: 22,
|
|
483
|
+
DJ: 27,
|
|
484
|
+
DK: 18,
|
|
485
|
+
DO: 28,
|
|
486
|
+
EE: 20,
|
|
487
|
+
EG: 29,
|
|
488
|
+
ES: 24,
|
|
489
|
+
FI: 18,
|
|
490
|
+
FK: 18,
|
|
491
|
+
FO: 18,
|
|
492
|
+
FR: 27,
|
|
493
|
+
GB: 22,
|
|
494
|
+
GE: 22,
|
|
495
|
+
GI: 23,
|
|
496
|
+
GL: 18,
|
|
497
|
+
GR: 27,
|
|
498
|
+
GT: 28,
|
|
499
|
+
HN: 28,
|
|
500
|
+
HR: 21,
|
|
501
|
+
HU: 28,
|
|
502
|
+
IE: 22,
|
|
503
|
+
IL: 23,
|
|
504
|
+
IQ: 23,
|
|
505
|
+
IS: 26,
|
|
506
|
+
IT: 27,
|
|
507
|
+
JO: 30,
|
|
508
|
+
KW: 30,
|
|
509
|
+
KZ: 20,
|
|
510
|
+
LB: 28,
|
|
511
|
+
LC: 32,
|
|
512
|
+
LI: 21,
|
|
513
|
+
LT: 20,
|
|
514
|
+
LU: 20,
|
|
515
|
+
LV: 21,
|
|
516
|
+
LY: 25,
|
|
517
|
+
MC: 27,
|
|
518
|
+
MD: 24,
|
|
519
|
+
ME: 22,
|
|
520
|
+
MK: 19,
|
|
521
|
+
MN: 20,
|
|
522
|
+
MR: 27,
|
|
523
|
+
MT: 31,
|
|
524
|
+
MU: 30,
|
|
525
|
+
NI: 28,
|
|
526
|
+
NL: 18,
|
|
527
|
+
NO: 15,
|
|
528
|
+
OM: 23,
|
|
529
|
+
PK: 24,
|
|
530
|
+
PL: 28,
|
|
531
|
+
PS: 29,
|
|
532
|
+
PT: 25,
|
|
533
|
+
QA: 29,
|
|
534
|
+
RO: 24,
|
|
535
|
+
RS: 22,
|
|
536
|
+
RU: 33,
|
|
537
|
+
SA: 24,
|
|
538
|
+
SC: 31,
|
|
539
|
+
SD: 18,
|
|
540
|
+
SE: 24,
|
|
541
|
+
SI: 19,
|
|
542
|
+
SK: 24,
|
|
543
|
+
SM: 27,
|
|
544
|
+
SN: 28,
|
|
545
|
+
SO: 23,
|
|
546
|
+
ST: 25,
|
|
547
|
+
SV: 28,
|
|
548
|
+
TL: 23,
|
|
549
|
+
TN: 24,
|
|
550
|
+
TR: 26,
|
|
551
|
+
UA: 29,
|
|
552
|
+
VA: 22,
|
|
553
|
+
VG: 24,
|
|
554
|
+
XK: 20,
|
|
555
|
+
YE: 30
|
|
556
|
+
};
|
|
557
|
+
function validateIban(raw) {
|
|
558
|
+
const s = raw.replace(/[ -]/g, "").toUpperCase();
|
|
559
|
+
if (!/^[A-Z]{2}\d{2}/.test(s)) return false;
|
|
560
|
+
const want = IBAN_LENGTH[s.slice(0, 2)];
|
|
561
|
+
if (want === void 0 || s.length < want) return false;
|
|
562
|
+
const iban = s.slice(0, want);
|
|
563
|
+
if (!/^[A-Z0-9]+$/.test(iban)) return false;
|
|
564
|
+
const rearranged = iban.slice(4) + iban.slice(0, 4);
|
|
565
|
+
let mod = 0;
|
|
566
|
+
for (const ch of rearranged) {
|
|
567
|
+
const code = ch.charCodeAt(0);
|
|
568
|
+
const digits = code >= 65 ? String(code - 55) : ch;
|
|
569
|
+
for (const d of digits) mod = (mod * 10 + (d.charCodeAt(0) - 48)) % 97;
|
|
570
|
+
}
|
|
571
|
+
return mod === 1;
|
|
572
|
+
}
|
|
573
|
+
var B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
574
|
+
var B58_INDEX = Object.fromEntries(
|
|
575
|
+
[...B58].map((c, i) => [c, i])
|
|
576
|
+
);
|
|
577
|
+
function validateBase58Check(s) {
|
|
578
|
+
if (!s) return null;
|
|
579
|
+
let n = 0n;
|
|
580
|
+
for (const c of s) {
|
|
581
|
+
const v = B58_INDEX[c];
|
|
582
|
+
if (v === void 0) return null;
|
|
583
|
+
n = n * 58n + BigInt(v);
|
|
584
|
+
}
|
|
585
|
+
let hex = n.toString(16);
|
|
586
|
+
if (hex.length % 2) hex = "0" + hex;
|
|
587
|
+
let zeros = 0;
|
|
588
|
+
for (const c of s) {
|
|
589
|
+
if (c !== "1") break;
|
|
590
|
+
zeros++;
|
|
591
|
+
}
|
|
592
|
+
const bytes = Buffer.concat([
|
|
593
|
+
Buffer.alloc(zeros),
|
|
594
|
+
n === 0n ? Buffer.alloc(0) : Buffer.from(hex, "hex")
|
|
595
|
+
]);
|
|
596
|
+
if (bytes.length < 5) return null;
|
|
597
|
+
const body = bytes.subarray(0, bytes.length - 4);
|
|
598
|
+
const check = bytes.subarray(bytes.length - 4);
|
|
599
|
+
const h = (0, import_crypto3.createHash)("sha256").update((0, import_crypto3.createHash)("sha256").update(body).digest()).digest();
|
|
600
|
+
return h.subarray(0, 4).equals(check) ? body : null;
|
|
601
|
+
}
|
|
602
|
+
function validateWif(s) {
|
|
603
|
+
const p = validateBase58Check(s);
|
|
604
|
+
if (!p || p[0] !== 128) return false;
|
|
605
|
+
return p.length === 33 || p.length === 34 && p[33] === 1;
|
|
606
|
+
}
|
|
607
|
+
var XPRV_VERSIONS = /* @__PURE__ */ new Set([76066276, 77428856, 78791436]);
|
|
608
|
+
function validateXprv(s) {
|
|
609
|
+
const p = validateBase58Check(s);
|
|
610
|
+
if (!p || p.length !== 78) return false;
|
|
611
|
+
const version = p.readUInt32BE(0);
|
|
612
|
+
return XPRV_VERSIONS.has(version) && p[45] === 0;
|
|
613
|
+
}
|
|
434
614
|
var ASSIGNMENT_CONTEXT_RE = /\b(?:password|passwd|secret|token|api[_-]?key|auth(?:_key|_token)?|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*/i;
|
|
435
615
|
function isAssignmentContext(text) {
|
|
436
616
|
return ASSIGNMENT_CONTEXT_RE.test(text);
|
|
@@ -623,6 +803,27 @@ var DLP_PATTERNS = [
|
|
|
623
803
|
severity: "block",
|
|
624
804
|
keywords: ["sg."]
|
|
625
805
|
},
|
|
806
|
+
// ── Cryptocurrency private keys (base58check-validated) ───────────────────
|
|
807
|
+
// Both are anchored with \b on each side: unanchored, `[KL][base58]{51}`
|
|
808
|
+
// matches INSIDE any longer base58 blob (an xprv, a Solana keypair, a
|
|
809
|
+
// Monero address). Lookbehind fails safe-regex2; \b is the house style
|
|
810
|
+
// (see the card regexes). Mainnet only, matching validateWif / validateXprv;
|
|
811
|
+
// testnet (WIF 0xEF, tprv) is deferred. Cost was measured: the WIF regex
|
|
812
|
+
// runs on every string (first keyword-less pattern) at 0.024 ms per 100 KB
|
|
813
|
+
// of prose, so no prefilter is warranted.
|
|
814
|
+
{
|
|
815
|
+
name: "Bitcoin WIF Private Key",
|
|
816
|
+
regex: /\b(?:5[1-9A-HJ-NP-Za-km-z]{50}|[KL][1-9A-HJ-NP-Za-km-z]{51})\b/,
|
|
817
|
+
severity: "block",
|
|
818
|
+
validate: validateWif
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
name: "Extended Private Key",
|
|
822
|
+
regex: /\b[xyz]prv[1-9A-HJ-NP-Za-km-z]{107}\b/,
|
|
823
|
+
severity: "block",
|
|
824
|
+
keywords: ["xprv", "yprv", "zprv"],
|
|
825
|
+
validate: validateXprv
|
|
826
|
+
},
|
|
626
827
|
// ── Private keys (PEM) ────────────────────────────────────────────────────
|
|
627
828
|
{
|
|
628
829
|
name: "Private Key (PEM)",
|
|
@@ -979,6 +1180,33 @@ function maskSecret(raw, pattern) {
|
|
|
979
1180
|
var MAX_DEPTH = 5;
|
|
980
1181
|
var MAX_STRING_BYTES = 1e5;
|
|
981
1182
|
var MAX_JSON_PARSE_BYTES = 1e4;
|
|
1183
|
+
function suppressed(pattern, raw) {
|
|
1184
|
+
if (pattern.validate) {
|
|
1185
|
+
let ok;
|
|
1186
|
+
try {
|
|
1187
|
+
ok = pattern.validate(raw);
|
|
1188
|
+
} catch {
|
|
1189
|
+
ok = true;
|
|
1190
|
+
}
|
|
1191
|
+
return !ok;
|
|
1192
|
+
}
|
|
1193
|
+
if (DLP_STOPWORDS.some((sw) => raw.toLowerCase().includes(sw))) return true;
|
|
1194
|
+
if (pattern.minEntropy !== void 0 && shannonEntropy(raw) < pattern.minEntropy) return true;
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
function firstAcceptedMatch(pattern, text) {
|
|
1198
|
+
const flags = pattern.regex.flags.includes("g") ? pattern.regex.flags : pattern.regex.flags + "g";
|
|
1199
|
+
const re = new RegExp(pattern.regex.source, flags);
|
|
1200
|
+
let m;
|
|
1201
|
+
while ((m = re.exec(text)) !== null) {
|
|
1202
|
+
if (m[0].length === 0) {
|
|
1203
|
+
re.lastIndex = m.index + 1;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
if (!suppressed(pattern, m[0])) return m[0];
|
|
1207
|
+
}
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
982
1210
|
function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
983
1211
|
if (depth > MAX_DEPTH || args === null || args === void 0) return null;
|
|
984
1212
|
if (Array.isArray(args)) {
|
|
@@ -1003,18 +1231,16 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
1003
1231
|
if (pattern.keywords && !pattern.keywords.some((kw) => textLower.includes(kw.toLowerCase()))) {
|
|
1004
1232
|
continue;
|
|
1005
1233
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
};
|
|
1017
|
-
}
|
|
1234
|
+
const raw = firstAcceptedMatch(pattern, text);
|
|
1235
|
+
if (raw === null) continue;
|
|
1236
|
+
const severity = pattern.contextBoost && assignmentCtx ? "block" : pattern.severity;
|
|
1237
|
+
return {
|
|
1238
|
+
patternName: pattern.name,
|
|
1239
|
+
fieldPath,
|
|
1240
|
+
// Mask the ACCEPTED token, not the first regex hit in the field.
|
|
1241
|
+
redactedSample: maskSecret(raw, pattern.regex),
|
|
1242
|
+
severity
|
|
1243
|
+
};
|
|
1018
1244
|
}
|
|
1019
1245
|
if (text.length < MAX_JSON_PARSE_BYTES) {
|
|
1020
1246
|
const trimmed = text.trim();
|
|
@@ -1078,13 +1304,13 @@ function matchesPattern(text, patterns) {
|
|
|
1078
1304
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1079
1305
|
}
|
|
1080
1306
|
var FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1081
|
-
function getNestedValue(obj,
|
|
1307
|
+
function getNestedValue(obj, path15) {
|
|
1082
1308
|
if (!obj || typeof obj !== "object") return null;
|
|
1083
|
-
const
|
|
1084
|
-
for (const seg of
|
|
1309
|
+
const segments2 = path15.split(".");
|
|
1310
|
+
for (const seg of segments2) {
|
|
1085
1311
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
1086
1312
|
}
|
|
1087
|
-
return
|
|
1313
|
+
return segments2.reduce((prev, curr) => prev?.[curr], obj);
|
|
1088
1314
|
}
|
|
1089
1315
|
function evaluateSmartConditions(args, rule) {
|
|
1090
1316
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -2052,6 +2278,50 @@ function extractShellDestinations(command) {
|
|
|
2052
2278
|
}
|
|
2053
2279
|
return out;
|
|
2054
2280
|
}
|
|
2281
|
+
function extractShellDestTokens(command) {
|
|
2282
|
+
const f = parseShared(command);
|
|
2283
|
+
if (f === PARSE_FAIL) return [];
|
|
2284
|
+
const out = [];
|
|
2285
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2286
|
+
try {
|
|
2287
|
+
syntax.Walk(f, (node) => {
|
|
2288
|
+
if (!node) return false;
|
|
2289
|
+
const n = node;
|
|
2290
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
2291
|
+
const callArgs = n.Args || [];
|
|
2292
|
+
if (callArgs.length === 0) return true;
|
|
2293
|
+
const name = (resolveWordLiteral(callArgs[0]) || "").toLowerCase();
|
|
2294
|
+
if (!NET_BINARIES.has(name)) return true;
|
|
2295
|
+
const rest = callArgs.slice(1).map((a) => resolveWordLiteral(a));
|
|
2296
|
+
for (const raw of destTokensForBinary(name, rest)) {
|
|
2297
|
+
if (!raw) continue;
|
|
2298
|
+
let tok = raw.trim();
|
|
2299
|
+
const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(tok);
|
|
2300
|
+
const hasScheme = scheme !== null;
|
|
2301
|
+
if (hasScheme) tok = tok.slice(scheme[0].length);
|
|
2302
|
+
tok = tok.split(/[/?#]/)[0];
|
|
2303
|
+
const at = tok.lastIndexOf("@");
|
|
2304
|
+
if (at >= 0) tok = tok.slice(at + 1);
|
|
2305
|
+
if (tok.startsWith("[")) {
|
|
2306
|
+
const close = tok.indexOf("]");
|
|
2307
|
+
if (close > 0) tok = tok.slice(0, close + 1);
|
|
2308
|
+
} else {
|
|
2309
|
+
tok = tok.split(":")[0];
|
|
2310
|
+
}
|
|
2311
|
+
if (!tok) continue;
|
|
2312
|
+
if (!hasScheme && /^\d+$/.test(tok) && Number(tok) < 16777216) continue;
|
|
2313
|
+
const key = `${name}:${tok}`;
|
|
2314
|
+
if (seen.has(key)) continue;
|
|
2315
|
+
seen.add(key);
|
|
2316
|
+
out.push({ token: tok, binary: name });
|
|
2317
|
+
}
|
|
2318
|
+
return true;
|
|
2319
|
+
});
|
|
2320
|
+
} catch {
|
|
2321
|
+
return out;
|
|
2322
|
+
}
|
|
2323
|
+
return out;
|
|
2324
|
+
}
|
|
2055
2325
|
var FS_OP_CACHE_MAX = 5e3;
|
|
2056
2326
|
var fsOpCache = /* @__PURE__ */ new Map();
|
|
2057
2327
|
function analyzeFsOperation(command) {
|
|
@@ -2256,8 +2526,8 @@ function analyzeShellCommand(command) {
|
|
|
2256
2526
|
if (allTokens.length === 0) {
|
|
2257
2527
|
const normalized = command.replace(/\\(.)/g, "$1");
|
|
2258
2528
|
const sanitized = normalized.replace(/["'<>]/g, " ");
|
|
2259
|
-
const
|
|
2260
|
-
|
|
2529
|
+
const segments2 = sanitized.split(/[|;&]|\$\(|\)|`/);
|
|
2530
|
+
segments2.forEach((segment) => {
|
|
2261
2531
|
const tokens = segment.trim().split(/\s+/).filter(Boolean);
|
|
2262
2532
|
if (tokens.length > 0) {
|
|
2263
2533
|
const action = tokens[0].toLowerCase();
|
|
@@ -2420,7 +2690,7 @@ function isSensitivePath(p) {
|
|
|
2420
2690
|
return SENSITIVE_PATTERNS.some((re) => re.test(p));
|
|
2421
2691
|
}
|
|
2422
2692
|
function splitOnPipe(cmd) {
|
|
2423
|
-
const
|
|
2693
|
+
const segments2 = [];
|
|
2424
2694
|
let current = "";
|
|
2425
2695
|
let inSingle = false;
|
|
2426
2696
|
let inDouble = false;
|
|
@@ -2433,21 +2703,21 @@ function splitOnPipe(cmd) {
|
|
|
2433
2703
|
inDouble = !inDouble;
|
|
2434
2704
|
current += ch;
|
|
2435
2705
|
} else if (ch === "|" && !inSingle && !inDouble && cmd[i + 1] !== "|" && (i === 0 || cmd[i - 1] !== "|")) {
|
|
2436
|
-
|
|
2706
|
+
segments2.push(current.trim());
|
|
2437
2707
|
current = "";
|
|
2438
2708
|
} else {
|
|
2439
2709
|
current += ch;
|
|
2440
2710
|
}
|
|
2441
2711
|
}
|
|
2442
|
-
if (current.trim())
|
|
2443
|
-
return
|
|
2712
|
+
if (current.trim()) segments2.push(current.trim());
|
|
2713
|
+
return segments2.filter(Boolean);
|
|
2444
2714
|
}
|
|
2445
2715
|
function positionalTokens(segment) {
|
|
2446
2716
|
return segment.split(/\s+/).slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
|
|
2447
2717
|
}
|
|
2448
2718
|
function analyzePipeChain(command) {
|
|
2449
|
-
const
|
|
2450
|
-
if (
|
|
2719
|
+
const segments2 = splitOnPipe(command);
|
|
2720
|
+
if (segments2.length < 2) {
|
|
2451
2721
|
return {
|
|
2452
2722
|
isPipeline: false,
|
|
2453
2723
|
hasSensitiveSource: false,
|
|
@@ -2463,7 +2733,7 @@ function analyzePipeChain(command) {
|
|
|
2463
2733
|
let hasSensitiveSource = false;
|
|
2464
2734
|
let hasExternalSink = false;
|
|
2465
2735
|
let hasObfuscation = false;
|
|
2466
|
-
for (const segment of
|
|
2736
|
+
for (const segment of segments2) {
|
|
2467
2737
|
const tokens = segment.split(/\s+/).filter(Boolean);
|
|
2468
2738
|
if (tokens.length === 0) continue;
|
|
2469
2739
|
const binary = tokens[0].toLowerCase();
|
|
@@ -2501,8 +2771,8 @@ function analyzePipeChain(command) {
|
|
|
2501
2771
|
};
|
|
2502
2772
|
}
|
|
2503
2773
|
function basename(p) {
|
|
2504
|
-
const
|
|
2505
|
-
return
|
|
2774
|
+
const segments2 = p.split(/[\\/]/);
|
|
2775
|
+
return segments2[segments2.length - 1] || "";
|
|
2506
2776
|
}
|
|
2507
2777
|
var FLAGS_WITH_VALUES = {
|
|
2508
2778
|
curl: /* @__PURE__ */ new Set([
|
|
@@ -2665,6 +2935,204 @@ function extractAllSshHosts(tokens) {
|
|
|
2665
2935
|
}
|
|
2666
2936
|
return [...hosts].filter(Boolean);
|
|
2667
2937
|
}
|
|
2938
|
+
var SSRF_MAX_HOST = 253;
|
|
2939
|
+
function parseComponent(s) {
|
|
2940
|
+
if (!s) return null;
|
|
2941
|
+
if (/^0[xX][0-9a-fA-F]+$/.test(s)) return parseInt(s.slice(2), 16);
|
|
2942
|
+
if (s === "0") return 0;
|
|
2943
|
+
if (/^0[0-7]+$/.test(s)) return parseInt(s.slice(1), 8);
|
|
2944
|
+
if (/^[1-9][0-9]*$/.test(s)) return Number(s);
|
|
2945
|
+
return null;
|
|
2946
|
+
}
|
|
2947
|
+
function parseIpv4(input) {
|
|
2948
|
+
let s = input;
|
|
2949
|
+
if (s.endsWith(".")) s = s.slice(0, -1);
|
|
2950
|
+
if (!s) return null;
|
|
2951
|
+
const parts = s.split(".");
|
|
2952
|
+
if (parts.length > 4) return null;
|
|
2953
|
+
const vals = [];
|
|
2954
|
+
for (const p of parts) {
|
|
2955
|
+
const v = parseComponent(p);
|
|
2956
|
+
if (v === null || !Number.isFinite(v) || v < 0) return null;
|
|
2957
|
+
vals.push(v);
|
|
2958
|
+
}
|
|
2959
|
+
const n = vals.length;
|
|
2960
|
+
for (let i = 0; i < n - 1; i++) if (vals[i] > 255) return null;
|
|
2961
|
+
const last = vals[n - 1];
|
|
2962
|
+
const remainingBytes = 4 - (n - 1);
|
|
2963
|
+
const limit = Math.pow(256, remainingBytes);
|
|
2964
|
+
if (last >= limit) return null;
|
|
2965
|
+
let value = last;
|
|
2966
|
+
for (let i = 0; i < n - 1; i++) value += vals[i] * Math.pow(256, 3 - i);
|
|
2967
|
+
if (value > 4294967295) return null;
|
|
2968
|
+
return [value >>> 24 & 255, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
|
|
2969
|
+
}
|
|
2970
|
+
function expandIpv6(input) {
|
|
2971
|
+
const s = input.toLowerCase();
|
|
2972
|
+
if (!/^[0-9a-f:.]+$/.test(s)) return null;
|
|
2973
|
+
if ((s.match(/::/g) ?? []).length > 1) return null;
|
|
2974
|
+
let head = s;
|
|
2975
|
+
let tailV4 = null;
|
|
2976
|
+
const lastColon = s.lastIndexOf(":");
|
|
2977
|
+
const afterLast = s.slice(lastColon + 1);
|
|
2978
|
+
if (afterLast.includes(".")) {
|
|
2979
|
+
const dotted = parseIpv4(afterLast);
|
|
2980
|
+
if (!dotted) return null;
|
|
2981
|
+
const o = dotted.split(".").map(Number);
|
|
2982
|
+
tailV4 = [o[0] << 8 | o[1], o[2] << 8 | o[3]];
|
|
2983
|
+
head = s.slice(0, lastColon + 1) + "0";
|
|
2984
|
+
}
|
|
2985
|
+
const [lhs, rhs] = head.includes("::") ? head.split("::") : [head, null];
|
|
2986
|
+
const toGroups = (part) => {
|
|
2987
|
+
if (!part) return [];
|
|
2988
|
+
const out = [];
|
|
2989
|
+
for (const g of part.split(":")) {
|
|
2990
|
+
if (!/^[0-9a-f]{1,4}$/.test(g)) return null;
|
|
2991
|
+
out.push(parseInt(g, 16));
|
|
2992
|
+
}
|
|
2993
|
+
return out;
|
|
2994
|
+
};
|
|
2995
|
+
const left = toGroups(lhs);
|
|
2996
|
+
if (left === null) return null;
|
|
2997
|
+
let right = [];
|
|
2998
|
+
if (rhs !== null) {
|
|
2999
|
+
const r = toGroups(rhs);
|
|
3000
|
+
if (r === null) return null;
|
|
3001
|
+
right = r;
|
|
3002
|
+
}
|
|
3003
|
+
if (tailV4) {
|
|
3004
|
+
if (rhs !== null) right = right.slice(0, -1).concat(tailV4);
|
|
3005
|
+
else left.splice(left.length - 1, 1, ...tailV4);
|
|
3006
|
+
}
|
|
3007
|
+
const groups = rhs === null ? left : left.concat(new Array(8 - left.length - right.length).fill(0), right);
|
|
3008
|
+
if (rhs === null && groups.length !== 8) return null;
|
|
3009
|
+
if (rhs !== null && left.length + right.length > 8) return null;
|
|
3010
|
+
if (groups.length !== 8) return null;
|
|
3011
|
+
return groups;
|
|
3012
|
+
}
|
|
3013
|
+
function compressIpv6(g) {
|
|
3014
|
+
let bestStart = -1;
|
|
3015
|
+
let bestLen = 0;
|
|
3016
|
+
let i = 0;
|
|
3017
|
+
while (i < 8) {
|
|
3018
|
+
if (g[i] !== 0) {
|
|
3019
|
+
i++;
|
|
3020
|
+
continue;
|
|
3021
|
+
}
|
|
3022
|
+
let j = i;
|
|
3023
|
+
while (j < 8 && g[j] === 0) j++;
|
|
3024
|
+
if (j - i > bestLen) {
|
|
3025
|
+
bestLen = j - i;
|
|
3026
|
+
bestStart = i;
|
|
3027
|
+
}
|
|
3028
|
+
i = j;
|
|
3029
|
+
}
|
|
3030
|
+
const hex = g.map((x) => x.toString(16));
|
|
3031
|
+
if (bestLen < 2) return hex.join(":");
|
|
3032
|
+
return hex.slice(0, bestStart).join(":") + "::" + hex.slice(bestStart + bestLen).join(":");
|
|
3033
|
+
}
|
|
3034
|
+
function normalizeIpLiteral(host) {
|
|
3035
|
+
try {
|
|
3036
|
+
if (typeof host !== "string") return null;
|
|
3037
|
+
let s = host.trim();
|
|
3038
|
+
if (!s || s.length > SSRF_MAX_HOST) return null;
|
|
3039
|
+
if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
|
|
3040
|
+
const zone = s.indexOf("%");
|
|
3041
|
+
if (zone >= 0) s = s.slice(0, zone);
|
|
3042
|
+
if (!s) return null;
|
|
3043
|
+
if (s.includes(":")) {
|
|
3044
|
+
const g = expandIpv6(s);
|
|
3045
|
+
if (!g) return null;
|
|
3046
|
+
const mapped = g.slice(0, 5).every((x) => x === 0) && g[5] === 65535;
|
|
3047
|
+
if (mapped) {
|
|
3048
|
+
return [g[6] >> 8 & 255, g[6] & 255, g[7] >> 8 & 255, g[7] & 255].join(".");
|
|
3049
|
+
}
|
|
3050
|
+
return compressIpv6(g);
|
|
3051
|
+
}
|
|
3052
|
+
return parseIpv4(s);
|
|
3053
|
+
} catch {
|
|
3054
|
+
return null;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
|
|
3058
|
+
"169.254.169.254",
|
|
3059
|
+
// AWS / Azure / DigitalOcean / OpenStack IMDS
|
|
3060
|
+
"169.254.170.2",
|
|
3061
|
+
// AWS ECS task role
|
|
3062
|
+
"168.63.129.16",
|
|
3063
|
+
// Azure WireServer
|
|
3064
|
+
"fd00:ec2::254"
|
|
3065
|
+
// AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
|
|
3066
|
+
]);
|
|
3067
|
+
var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
|
|
3068
|
+
var v4Octets = (a) => {
|
|
3069
|
+
const p = a.split(".");
|
|
3070
|
+
return p.length === 4 ? p.map(Number) : null;
|
|
3071
|
+
};
|
|
3072
|
+
function classifySsrf(host) {
|
|
3073
|
+
try {
|
|
3074
|
+
if (typeof host !== "string" || !host) return null;
|
|
3075
|
+
const lower = host.trim().toLowerCase().replace(/\.$/, "");
|
|
3076
|
+
const ip = normalizeIpLiteral(host);
|
|
3077
|
+
if (ip === null) {
|
|
3078
|
+
return METADATA_HOSTNAMES.has(lower) ? { tier: "metadata", overridable: false, kind: "hostname" } : null;
|
|
3079
|
+
}
|
|
3080
|
+
const hit = (tier, overridable) => ({
|
|
3081
|
+
tier,
|
|
3082
|
+
overridable,
|
|
3083
|
+
kind: "address",
|
|
3084
|
+
normalized: ip
|
|
3085
|
+
});
|
|
3086
|
+
if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
|
|
3087
|
+
const o = v4Octets(ip);
|
|
3088
|
+
if (o) {
|
|
3089
|
+
if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", false);
|
|
3090
|
+
if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
|
|
3091
|
+
if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
|
|
3092
|
+
if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
|
|
3093
|
+
if (o[0] === 127) return hit("private", true);
|
|
3094
|
+
if (o[0] === 10) return hit("private", true);
|
|
3095
|
+
if (o[0] === 192 && o[1] === 168) return hit("private", true);
|
|
3096
|
+
if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return hit("private", true);
|
|
3097
|
+
return null;
|
|
3098
|
+
}
|
|
3099
|
+
const g = expandIpv6(ip);
|
|
3100
|
+
if (!g) return null;
|
|
3101
|
+
if (g.every((x) => x === 0)) return hit("unspecified", false);
|
|
3102
|
+
if ((g[0] & 65472) === 65152) return hit("link-local", false);
|
|
3103
|
+
if ((g[0] & 65280) === 65280) return hit("multicast", false);
|
|
3104
|
+
if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
|
|
3105
|
+
return null;
|
|
3106
|
+
} catch {
|
|
3107
|
+
return null;
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
var TIER_REASON = {
|
|
3111
|
+
metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
|
|
3112
|
+
"link-local": "a link-local address",
|
|
3113
|
+
multicast: "a multicast address",
|
|
3114
|
+
unspecified: "the unspecified address",
|
|
3115
|
+
cgnat: "a carrier-grade NAT address",
|
|
3116
|
+
private: "a loopback or private address"
|
|
3117
|
+
};
|
|
3118
|
+
function ssrfFloor(tokens, opts = {}) {
|
|
3119
|
+
const exempt = new Set(
|
|
3120
|
+
(opts.ssrfAllow ?? []).map((e) => normalizeIpLiteral(e) ?? e.trim().toLowerCase())
|
|
3121
|
+
);
|
|
3122
|
+
for (const { token, binary } of tokens) {
|
|
3123
|
+
const m = classifySsrf(token);
|
|
3124
|
+
if (!m) continue;
|
|
3125
|
+
if (m.tier === "private" && !opts.ssrfStrict) continue;
|
|
3126
|
+
if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
|
|
3127
|
+
return {
|
|
3128
|
+
...m,
|
|
3129
|
+
host: token,
|
|
3130
|
+
binary,
|
|
3131
|
+
reason: `Blocked: ${token} is ${TIER_REASON[m.tier]}` + (m.normalized && m.normalized !== token ? ` (${m.normalized})` : "") + (m.overridable ? "." : ". This address cannot be allowlisted.")
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
return null;
|
|
3135
|
+
}
|
|
2668
3136
|
function resolveCheck(v) {
|
|
2669
3137
|
return v === "off" || v === "block" ? v : "review";
|
|
2670
3138
|
}
|
|
@@ -2897,6 +3365,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2897
3365
|
}
|
|
2898
3366
|
const builtin = strictestVerdict(candidates);
|
|
2899
3367
|
if (builtin) return builtin;
|
|
3368
|
+
{
|
|
3369
|
+
const ssrf = ssrfFloor(extractShellDestTokens(shellCommand), {
|
|
3370
|
+
ssrfAllow: config.policy.egress?.ssrfAllow,
|
|
3371
|
+
ssrfStrict: config.policy.egress?.ssrfStrict
|
|
3372
|
+
});
|
|
3373
|
+
if (ssrf) {
|
|
3374
|
+
return {
|
|
3375
|
+
decision: "block",
|
|
3376
|
+
blockedByLabel: "\u{1F310} Node9 Egress (Protected Address)",
|
|
3377
|
+
reason: ssrf.reason,
|
|
3378
|
+
ruleName: `ssrf:${ssrf.tier}:${ssrf.binary}:${ssrf.host}`,
|
|
3379
|
+
ruleDescription: ssrf.reason,
|
|
3380
|
+
tier: 3
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
2900
3384
|
if (config.policy.egress?.enabled) {
|
|
2901
3385
|
const dests = extractShellDestinations(shellCommand);
|
|
2902
3386
|
if (dests.length > 0) {
|
|
@@ -3824,7 +4308,7 @@ assertBuiltinShieldRegexesAreSafe();
|
|
|
3824
4308
|
var LOOP_MAX_RECORDS = 500;
|
|
3825
4309
|
function computeArgsHash(args) {
|
|
3826
4310
|
const str = JSON.stringify(args ?? "");
|
|
3827
|
-
return
|
|
4311
|
+
return import_crypto4.default.createHash("sha256").update(str).digest("hex").slice(0, 16);
|
|
3828
4312
|
}
|
|
3829
4313
|
function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
|
|
3830
4314
|
const hash = computeArgsHash(args);
|
|
@@ -3838,28 +4322,236 @@ function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
|
|
|
3838
4322
|
var PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
3839
4323
|
var PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
3840
4324
|
var PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
|
|
3841
|
-
var
|
|
4325
|
+
var 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/;
|
|
4326
|
+
var PII_CC15_RE = /\b3[47]\d{2}[-\s]?\d{6}[-\s]?\d{5}\b/;
|
|
4327
|
+
var PII_IBAN_RE = /\b[A-Z]{2}\d{2}(?:[A-Z0-9]|[ -][A-Z0-9]){11,30}\b/;
|
|
4328
|
+
function hasValidCard(text) {
|
|
4329
|
+
for (const base of [PII_CC16_RE, PII_CC15_RE]) {
|
|
4330
|
+
const re = new RegExp(base.source, "g");
|
|
4331
|
+
let m;
|
|
4332
|
+
while ((m = re.exec(text)) !== null) {
|
|
4333
|
+
if (validateLuhn(m[0].replace(/\D/g, ""))) return true;
|
|
4334
|
+
re.lastIndex = m.index + 1;
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
return false;
|
|
4338
|
+
}
|
|
4339
|
+
function hasValidIban(text) {
|
|
4340
|
+
const re = new RegExp(PII_IBAN_RE.source, "g");
|
|
4341
|
+
let m;
|
|
4342
|
+
while ((m = re.exec(text)) !== null) {
|
|
4343
|
+
if (validateIban(m[0])) return true;
|
|
4344
|
+
re.lastIndex = m.index + 1;
|
|
4345
|
+
}
|
|
4346
|
+
return false;
|
|
4347
|
+
}
|
|
3842
4348
|
function detectPii(text) {
|
|
3843
4349
|
const found = /* @__PURE__ */ new Set();
|
|
3844
4350
|
if (/@/.test(text) && PII_EMAIL_RE.test(text)) found.add("Email");
|
|
3845
4351
|
if (/-/.test(text) && PII_SSN_RE.test(text)) found.add("SSN");
|
|
3846
4352
|
if (PII_PHONE_RE.test(text)) found.add("Phone");
|
|
3847
|
-
if (
|
|
4353
|
+
if (hasValidCard(text)) found.add("Credit Card");
|
|
4354
|
+
if (hasValidIban(text)) found.add("IBAN");
|
|
3848
4355
|
return [...found];
|
|
3849
4356
|
}
|
|
3850
|
-
var REALTIME_PII_PATTERNS = ["SSN", "Credit Card"];
|
|
4357
|
+
var REALTIME_PII_PATTERNS = ["SSN", "Credit Card", "IBAN"];
|
|
3851
4358
|
var MAX_PII_SCAN_BYTES = 1e5;
|
|
4359
|
+
function* stringLeaves(v, depth = 0) {
|
|
4360
|
+
if (depth > 6) return;
|
|
4361
|
+
if (typeof v === "string") {
|
|
4362
|
+
if (v.length > 0) yield v;
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
if (typeof v === "number") {
|
|
4366
|
+
if (Number.isFinite(v)) yield String(v);
|
|
4367
|
+
return;
|
|
4368
|
+
}
|
|
4369
|
+
if (!v || typeof v !== "object") return;
|
|
4370
|
+
if (Array.isArray(v)) {
|
|
4371
|
+
for (const x of v) yield* stringLeaves(x, depth + 1);
|
|
4372
|
+
return;
|
|
4373
|
+
}
|
|
4374
|
+
for (const x of Object.values(v)) yield* stringLeaves(x, depth + 1);
|
|
4375
|
+
}
|
|
3852
4376
|
function detectArgsPii(args) {
|
|
3853
4377
|
if (args === null || args === void 0) return [];
|
|
3854
|
-
|
|
4378
|
+
const found = /* @__PURE__ */ new Set();
|
|
4379
|
+
let budget = MAX_PII_SCAN_BYTES;
|
|
3855
4380
|
try {
|
|
3856
|
-
|
|
4381
|
+
for (const leaf of stringLeaves(args)) {
|
|
4382
|
+
if (budget <= 0) break;
|
|
4383
|
+
const t = leaf.length > budget ? leaf.slice(0, budget) : leaf;
|
|
4384
|
+
budget -= t.length;
|
|
4385
|
+
for (const p of detectPii(t)) {
|
|
4386
|
+
if (REALTIME_PII_PATTERNS.includes(p)) found.add(p);
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
3857
4389
|
} catch {
|
|
3858
4390
|
return [];
|
|
3859
4391
|
}
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
4392
|
+
return [...found];
|
|
4393
|
+
}
|
|
4394
|
+
var CANARY_MIN_LENGTH = 16;
|
|
4395
|
+
var MAX_TEXT = 1e5;
|
|
4396
|
+
var MAX_DEPTH2 = 6;
|
|
4397
|
+
var MAX_JSON_PARSE = 1e4;
|
|
4398
|
+
var URL_DEPTH = 4;
|
|
4399
|
+
var B64_DEPTH = 3;
|
|
4400
|
+
var MIN_SEGMENT = 16;
|
|
4401
|
+
var SEPARATORS = /[./\\?&= \t\n\r:;,\-_@%+#]/g;
|
|
4402
|
+
var stripSeparators = (s) => s.replace(SEPARATORS, "");
|
|
4403
|
+
function percentDecodeOnce(s) {
|
|
4404
|
+
try {
|
|
4405
|
+
return decodeURIComponent(s);
|
|
4406
|
+
} catch {
|
|
4407
|
+
return s.replace(/%([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
function segments(s, alphabet) {
|
|
4411
|
+
const out = /* @__PURE__ */ new Set();
|
|
4412
|
+
if (alphabet.test(s)) out.add(s);
|
|
4413
|
+
for (const seg of s.split(/[?&\s"'<>]+/)) {
|
|
4414
|
+
if (seg.length >= MIN_SEGMENT && alphabet.test(seg)) out.add(seg);
|
|
4415
|
+
for (const part of seg.split("=")) {
|
|
4416
|
+
if (part.length >= MIN_SEGMENT && alphabet.test(part)) out.add(part);
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
return [...out];
|
|
4420
|
+
}
|
|
4421
|
+
var looksText = (s) => s.length > 0 && !/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(s);
|
|
4422
|
+
var CANARY_DECODERS = {
|
|
4423
|
+
url: (s) => {
|
|
4424
|
+
const out = [];
|
|
4425
|
+
let cur = s;
|
|
4426
|
+
for (let i = 0; i < URL_DEPTH; i++) {
|
|
4427
|
+
const d = percentDecodeOnce(cur);
|
|
4428
|
+
if (d === cur) break;
|
|
4429
|
+
out.push(d);
|
|
4430
|
+
cur = d;
|
|
4431
|
+
}
|
|
4432
|
+
return out;
|
|
4433
|
+
},
|
|
4434
|
+
base64: (s) => {
|
|
4435
|
+
const out = [];
|
|
4436
|
+
let frontier = segments(s, /^[A-Za-z0-9+/\-_=]+$/);
|
|
4437
|
+
for (let depth = 0; depth < B64_DEPTH && frontier.length; depth++) {
|
|
4438
|
+
const next = [];
|
|
4439
|
+
for (const c of frontier) {
|
|
4440
|
+
const d = Buffer.from(c, "base64").toString("utf8");
|
|
4441
|
+
if (!looksText(d) || d.length < CANARY_MIN_LENGTH) continue;
|
|
4442
|
+
out.push(d);
|
|
4443
|
+
next.push(...segments(d, /^[A-Za-z0-9+/\-_=]+$/));
|
|
4444
|
+
}
|
|
4445
|
+
frontier = next;
|
|
4446
|
+
}
|
|
4447
|
+
return out;
|
|
4448
|
+
},
|
|
4449
|
+
hex: (s) => {
|
|
4450
|
+
const out = [];
|
|
4451
|
+
for (const c of segments(s, /^[0-9A-Fa-f]+$/)) {
|
|
4452
|
+
if (c.length % 2 !== 0 || c.length < CANARY_MIN_LENGTH * 2) continue;
|
|
4453
|
+
const d = Buffer.from(c, "hex").toString("utf8");
|
|
4454
|
+
if (looksText(d)) out.push(d);
|
|
4455
|
+
}
|
|
4456
|
+
return out;
|
|
4457
|
+
},
|
|
4458
|
+
separators: (s) => [stripSeparators(s)]
|
|
4459
|
+
};
|
|
4460
|
+
function lowestOffset(cands, needles, stripped) {
|
|
4461
|
+
let best = null;
|
|
4462
|
+
for (const c of cands) {
|
|
4463
|
+
for (const n of needles) {
|
|
4464
|
+
const off = c.indexOf(stripped ? n.stripped : n.raw);
|
|
4465
|
+
if (off >= 0 && (best === null || off < best.off)) best = { off, v: n.v };
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
return best?.v ?? null;
|
|
4469
|
+
}
|
|
4470
|
+
var VIEWS = [
|
|
4471
|
+
{ view: "url-decoded", decoder: "url", stripped: false },
|
|
4472
|
+
{ view: "base64-decoded", decoder: "base64", stripped: false },
|
|
4473
|
+
{ view: "hex-decoded", decoder: "hex", stripped: false },
|
|
4474
|
+
{ view: "separators-stripped", decoder: "separators", stripped: true }
|
|
4475
|
+
];
|
|
4476
|
+
function prepare(values) {
|
|
4477
|
+
const out = [];
|
|
4478
|
+
for (const v of values) {
|
|
4479
|
+
if (typeof v.value !== "string" || v.value.length < CANARY_MIN_LENGTH) {
|
|
4480
|
+
console.error(
|
|
4481
|
+
`[node9 engine] canary ${v.id}: value shorter than ${CANARY_MIN_LENGTH}, skipped`
|
|
4482
|
+
);
|
|
4483
|
+
continue;
|
|
4484
|
+
}
|
|
4485
|
+
out.push({ v, raw: v.value, stripped: stripSeparators(v.value) });
|
|
4486
|
+
}
|
|
4487
|
+
return out;
|
|
4488
|
+
}
|
|
4489
|
+
function matchPrepared(text, needles) {
|
|
4490
|
+
if (!text || needles.length === 0) return null;
|
|
4491
|
+
const t = text.length > MAX_TEXT ? text.slice(0, MAX_TEXT) : text;
|
|
4492
|
+
const raw = lowestOffset([t], needles, false);
|
|
4493
|
+
if (raw) return { v: raw, view: "raw" };
|
|
4494
|
+
for (const { view, decoder, stripped } of VIEWS) {
|
|
4495
|
+
let cands;
|
|
4496
|
+
try {
|
|
4497
|
+
cands = CANARY_DECODERS[decoder](t);
|
|
4498
|
+
} catch (e) {
|
|
4499
|
+
console.error(
|
|
4500
|
+
`[node9 engine] canary view ${view} failed, skipped:`,
|
|
4501
|
+
e instanceof Error ? e.message : String(e)
|
|
4502
|
+
);
|
|
4503
|
+
continue;
|
|
4504
|
+
}
|
|
4505
|
+
const hit = lowestOffset(cands, needles, stripped);
|
|
4506
|
+
if (hit) return { v: hit, view };
|
|
4507
|
+
}
|
|
4508
|
+
return null;
|
|
4509
|
+
}
|
|
4510
|
+
function matchCanaryArgs(args, values) {
|
|
4511
|
+
if (values.length === 0) return null;
|
|
4512
|
+
const needles = prepare(values);
|
|
4513
|
+
if (needles.length === 0) return null;
|
|
4514
|
+
const walk = (v, depth, fieldPath) => {
|
|
4515
|
+
if (depth > MAX_DEPTH2) return null;
|
|
4516
|
+
if (typeof v === "string") {
|
|
4517
|
+
const hit = matchPrepared(v, needles);
|
|
4518
|
+
if (hit) return { id: hit.v.id, view: hit.view, fieldPath, retired: Boolean(hit.v.retired) };
|
|
4519
|
+
if (v.length < MAX_JSON_PARSE) {
|
|
4520
|
+
const trimmed = v.trim();
|
|
4521
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
4522
|
+
try {
|
|
4523
|
+
return walk(JSON.parse(v), depth + 1, fieldPath);
|
|
4524
|
+
} catch {
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
return null;
|
|
4529
|
+
}
|
|
4530
|
+
if (typeof v === "number" && Number.isFinite(v)) return walk(String(v), depth, fieldPath);
|
|
4531
|
+
if (Array.isArray(v)) {
|
|
4532
|
+
for (let i = 0; i < v.length; i++) {
|
|
4533
|
+
const h = walk(v[i], depth + 1, `${fieldPath}[${i}]`);
|
|
4534
|
+
if (h) return h;
|
|
4535
|
+
}
|
|
4536
|
+
return null;
|
|
4537
|
+
}
|
|
4538
|
+
if (v && typeof v === "object") {
|
|
4539
|
+
for (const [k, child] of Object.entries(v)) {
|
|
4540
|
+
const h = walk(child, depth + 1, fieldPath ? `${fieldPath}.${k}` : k);
|
|
4541
|
+
if (h) return h;
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4544
|
+
return null;
|
|
4545
|
+
};
|
|
4546
|
+
try {
|
|
4547
|
+
return walk(args, 0, "");
|
|
4548
|
+
} catch (e) {
|
|
4549
|
+
console.error(
|
|
4550
|
+
"[node9 engine] canary args walk failed:",
|
|
4551
|
+
e instanceof Error ? e.message : String(e)
|
|
4552
|
+
);
|
|
4553
|
+
return null;
|
|
4554
|
+
}
|
|
3863
4555
|
}
|
|
3864
4556
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
3865
4557
|
|
|
@@ -3989,6 +4681,12 @@ function applyManagedEgress(local, managed, locked, localModeUserSet = true) {
|
|
|
3989
4681
|
if (Array.isArray(managed.deny) && managed.deny.length > 0) {
|
|
3990
4682
|
next.deny = [.../* @__PURE__ */ new Set([...local.deny ?? [], ...managed.deny])];
|
|
3991
4683
|
}
|
|
4684
|
+
if (typeof managed.ssrfStrict === "boolean") {
|
|
4685
|
+
next.ssrfStrict = managed.ssrfStrict;
|
|
4686
|
+
}
|
|
4687
|
+
if (Array.isArray(managed.ssrfAllow)) {
|
|
4688
|
+
next.ssrfAllow = [...managed.ssrfAllow];
|
|
4689
|
+
}
|
|
3992
4690
|
if (typeof managed.allowPrivate === "boolean") {
|
|
3993
4691
|
next.allowPrivate = locked.includes("egressAllowPrivate") ? managed.allowPrivate : (local.allowPrivate ?? true) && managed.allowPrivate;
|
|
3994
4692
|
}
|
|
@@ -4070,9 +4768,9 @@ var B = "[\\s/\\\\]";
|
|
|
4070
4768
|
var SEP = "[/\\\\]";
|
|
4071
4769
|
function pathToRegexFragment(rawPath) {
|
|
4072
4770
|
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
4073
|
-
const
|
|
4074
|
-
if (
|
|
4075
|
-
return `(^|${B})${
|
|
4771
|
+
const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
4772
|
+
if (segments2.length === 0) return "";
|
|
4773
|
+
return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
|
|
4076
4774
|
}
|
|
4077
4775
|
function pathMatchesFragment(candidate, rawPath) {
|
|
4078
4776
|
const value = pathToRegexFragment(rawPath);
|
|
@@ -4177,6 +4875,20 @@ function isTrustedHost(host) {
|
|
|
4177
4875
|
}
|
|
4178
4876
|
|
|
4179
4877
|
// src/config/index.ts
|
|
4878
|
+
function sanitizeSsrfAllow(entries, source) {
|
|
4879
|
+
const kept = [];
|
|
4880
|
+
for (const entry of entries) {
|
|
4881
|
+
const m = classifySsrf(entry);
|
|
4882
|
+
if (m && !m.overridable) {
|
|
4883
|
+
process.emitWarning(
|
|
4884
|
+
`[node9] ${source} ssrfAllow entry "${entry}" is a protected address (${m.tier}) and cannot be exempted; ignoring it.`
|
|
4885
|
+
);
|
|
4886
|
+
continue;
|
|
4887
|
+
}
|
|
4888
|
+
kept.push(entry);
|
|
4889
|
+
}
|
|
4890
|
+
return kept;
|
|
4891
|
+
}
|
|
4180
4892
|
var DANGEROUS_WORDS = [
|
|
4181
4893
|
"mkfs",
|
|
4182
4894
|
// formats/wipes a filesystem partition
|
|
@@ -4186,6 +4898,7 @@ var DANGEROUS_WORDS = [
|
|
|
4186
4898
|
var DEFAULT_CONFIG = {
|
|
4187
4899
|
version: "1.0",
|
|
4188
4900
|
policySource: "local",
|
|
4901
|
+
ssrfStrictSource: "default",
|
|
4189
4902
|
settings: {
|
|
4190
4903
|
mode: "standard",
|
|
4191
4904
|
autoStartDaemon: true,
|
|
@@ -4369,7 +5082,17 @@ var DEFAULT_CONFIG = {
|
|
|
4369
5082
|
}
|
|
4370
5083
|
],
|
|
4371
5084
|
dlp: { enabled: true, scanIgnoredTools: true, pii: "off" },
|
|
4372
|
-
egress: {
|
|
5085
|
+
egress: {
|
|
5086
|
+
enabled: false,
|
|
5087
|
+
mode: "review",
|
|
5088
|
+
allow: [],
|
|
5089
|
+
deny: [],
|
|
5090
|
+
allowPrivate: true,
|
|
5091
|
+
// The SSRF floor is always on and needs no default; these two only
|
|
5092
|
+
// widen or narrow it. See doc/roadmap/active/ssrf-floor-design.md.
|
|
5093
|
+
ssrfAllow: [],
|
|
5094
|
+
ssrfStrict: false
|
|
5095
|
+
},
|
|
4373
5096
|
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
|
|
4374
5097
|
injectionScan: { enabled: false, minConfidence: "medium", allow: [] },
|
|
4375
5098
|
skillPinning: { enabled: false, mode: "warn", roots: [] },
|
|
@@ -4552,7 +5275,8 @@ function getConfig(cwd) {
|
|
|
4552
5275
|
egress: {
|
|
4553
5276
|
...DEFAULT_CONFIG.policy.egress,
|
|
4554
5277
|
allow: [...DEFAULT_CONFIG.policy.egress.allow],
|
|
4555
|
-
deny: [...DEFAULT_CONFIG.policy.egress.deny]
|
|
5278
|
+
deny: [...DEFAULT_CONFIG.policy.egress.deny],
|
|
5279
|
+
ssrfAllow: [...DEFAULT_CONFIG.policy.egress.ssrfAllow ?? []]
|
|
4556
5280
|
},
|
|
4557
5281
|
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
|
|
4558
5282
|
injectionScan: {
|
|
@@ -4578,6 +5302,7 @@ function getConfig(cwd) {
|
|
|
4578
5302
|
};
|
|
4579
5303
|
const pr2Creds = getCredentials();
|
|
4580
5304
|
const keyed = !!pr2Creds?.apiKey && pr2Creds.localOnly !== true;
|
|
5305
|
+
let ssrfStrictSource = "default";
|
|
4581
5306
|
const applyLayer = (source, isProject = false, isCloud = false) => {
|
|
4582
5307
|
if (!source) return;
|
|
4583
5308
|
const s = source.settings || {};
|
|
@@ -4660,6 +5385,15 @@ function getConfig(cwd) {
|
|
|
4660
5385
|
if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
|
|
4661
5386
|
if (e.allowPrivate !== void 0 && !(isProject && e.allowPrivate === true))
|
|
4662
5387
|
mergedPolicy.egress.allowPrivate = e.allowPrivate;
|
|
5388
|
+
if (!isProject) {
|
|
5389
|
+
if (Array.isArray(e.ssrfAllow)) {
|
|
5390
|
+
mergedPolicy.egress.ssrfAllow = sanitizeSsrfAllow(e.ssrfAllow, "egress.");
|
|
5391
|
+
}
|
|
5392
|
+
if (e.ssrfStrict !== void 0) {
|
|
5393
|
+
mergedPolicy.egress.ssrfStrict = e.ssrfStrict;
|
|
5394
|
+
ssrfStrictSource = "local";
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
4663
5397
|
}
|
|
4664
5398
|
if (p.loopDetection) {
|
|
4665
5399
|
const ld = p.loopDetection;
|
|
@@ -4766,6 +5500,13 @@ function getConfig(cwd) {
|
|
|
4766
5500
|
if (deny) mergedPolicy.egress.deny = deny;
|
|
4767
5501
|
if (typeof e.allowPrivate === "boolean")
|
|
4768
5502
|
mergedPolicy.egress.allowPrivate = e.allowPrivate;
|
|
5503
|
+
if (typeof e.ssrfStrict === "boolean") {
|
|
5504
|
+
mergedPolicy.egress.ssrfStrict = e.ssrfStrict;
|
|
5505
|
+
ssrfStrictSource = "workspace";
|
|
5506
|
+
}
|
|
5507
|
+
const ssrfAllow = hosts(e.ssrfAllow);
|
|
5508
|
+
if (ssrfAllow)
|
|
5509
|
+
mergedPolicy.egress.ssrfAllow = sanitizeSsrfAllow(ssrfAllow, "managed egress.");
|
|
4769
5510
|
} else {
|
|
4770
5511
|
mergedPolicy.egress = applyManagedEgress(
|
|
4771
5512
|
mergedPolicy.egress,
|
|
@@ -4774,7 +5515,16 @@ function getConfig(cwd) {
|
|
|
4774
5515
|
mode: typeof mc.egress.mode === "string" ? mc.egress.mode : void 0,
|
|
4775
5516
|
allow: hosts(mc.egress.allow),
|
|
4776
5517
|
deny: hosts(mc.egress.deny),
|
|
4777
|
-
allowPrivate: typeof mc.egress.allowPrivate === "boolean" ? mc.egress.allowPrivate : void 0
|
|
5518
|
+
allowPrivate: typeof mc.egress.allowPrivate === "boolean" ? mc.egress.allowPrivate : void 0,
|
|
5519
|
+
ssrfStrict: (() => {
|
|
5520
|
+
if (typeof mc.egress.ssrfStrict !== "boolean") return void 0;
|
|
5521
|
+
ssrfStrictSource = "workspace";
|
|
5522
|
+
return mc.egress.ssrfStrict;
|
|
5523
|
+
})(),
|
|
5524
|
+
ssrfAllow: (() => {
|
|
5525
|
+
const list = hosts(mc.egress.ssrfAllow);
|
|
5526
|
+
return list ? sanitizeSsrfAllow(list, "managed egress.") : void 0;
|
|
5527
|
+
})()
|
|
4778
5528
|
},
|
|
4779
5529
|
locked,
|
|
4780
5530
|
egressModeUserSet
|
|
@@ -4869,13 +5619,13 @@ function getConfig(cwd) {
|
|
|
4869
5619
|
}
|
|
4870
5620
|
if (Array.isArray(mc.jailPaths)) {
|
|
4871
5621
|
for (const jp of mc.jailPaths) {
|
|
4872
|
-
const
|
|
4873
|
-
if (!
|
|
5622
|
+
const path15 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
5623
|
+
if (!path15) continue;
|
|
4874
5624
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
4875
|
-
for (const r of pathRules(
|
|
5625
|
+
for (const r of pathRules(path15, verdict, "org-managed jail")) {
|
|
4876
5626
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
4877
5627
|
}
|
|
4878
|
-
mergedPolicy.managedJailPaths.push({ path:
|
|
5628
|
+
mergedPolicy.managedJailPaths.push({ path: path15, verdict });
|
|
4879
5629
|
}
|
|
4880
5630
|
}
|
|
4881
5631
|
if (Array.isArray(mc.trustedHosts)) {
|
|
@@ -5004,6 +5754,7 @@ function getConfig(cwd) {
|
|
|
5004
5754
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
5005
5755
|
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
5006
5756
|
mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
|
|
5757
|
+
const resolvedSsrfStrictSource = keyed && ssrfStrictSource === "local" ? "default" : ssrfStrictSource;
|
|
5007
5758
|
const result = {
|
|
5008
5759
|
settings: mergedSettings,
|
|
5009
5760
|
policy: mergedPolicy,
|
|
@@ -5011,7 +5762,10 @@ function getConfig(cwd) {
|
|
|
5011
5762
|
// PR-2 — the one truth introspection reads: which of the two working
|
|
5012
5763
|
// modes this machine is in. 'workspace' = keyed, policy from the cloud;
|
|
5013
5764
|
// 'local' = the local stack (incl. --local / named-profile keys).
|
|
5014
|
-
policySource: keyed ? "workspace" : "local"
|
|
5765
|
+
policySource: keyed ? "workspace" : "local",
|
|
5766
|
+
// A keyed machine drops the local policy layers wholesale, so a 'local'
|
|
5767
|
+
// provenance recorded before the fork cannot survive into the result.
|
|
5768
|
+
ssrfStrictSource: resolvedSsrfStrictSource
|
|
5015
5769
|
};
|
|
5016
5770
|
if (!cwd) cachedConfig = result;
|
|
5017
5771
|
return result;
|
|
@@ -5541,7 +6295,7 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
5541
6295
|
}
|
|
5542
6296
|
|
|
5543
6297
|
// src/auth/orchestrator.ts
|
|
5544
|
-
var
|
|
6298
|
+
var import_crypto5 = require("crypto");
|
|
5545
6299
|
|
|
5546
6300
|
// src/ui/native.ts
|
|
5547
6301
|
var import_child_process = require("child_process");
|
|
@@ -5877,13 +6631,91 @@ end run`;
|
|
|
5877
6631
|
});
|
|
5878
6632
|
}
|
|
5879
6633
|
|
|
5880
|
-
// src/
|
|
5881
|
-
|
|
6634
|
+
// src/canary/registry.ts
|
|
6635
|
+
var import_fs10 = __toESM(require("fs"));
|
|
6636
|
+
var import_os9 = __toESM(require("os"));
|
|
6637
|
+
var import_path12 = __toESM(require("path"));
|
|
5882
6638
|
|
|
5883
|
-
// src/
|
|
6639
|
+
// src/shields/jail.ts
|
|
5884
6640
|
var import_fs9 = __toESM(require("fs"));
|
|
5885
6641
|
var import_os8 = __toESM(require("os"));
|
|
5886
6642
|
var import_path11 = __toESM(require("path"));
|
|
6643
|
+
var USER_JAIL_SHIELD = "user-jail";
|
|
6644
|
+
function jailStorePath() {
|
|
6645
|
+
return import_path11.default.join(import_os8.default.homedir(), ".node9", "jail-paths.json");
|
|
6646
|
+
}
|
|
6647
|
+
function readJailPaths() {
|
|
6648
|
+
let text;
|
|
6649
|
+
try {
|
|
6650
|
+
text = import_fs9.default.readFileSync(jailStorePath(), "utf8");
|
|
6651
|
+
} catch (err) {
|
|
6652
|
+
if (err.code === "ENOENT") return [];
|
|
6653
|
+
throw err;
|
|
6654
|
+
}
|
|
6655
|
+
let parsed;
|
|
6656
|
+
try {
|
|
6657
|
+
parsed = JSON.parse(text);
|
|
6658
|
+
} catch {
|
|
6659
|
+
throw new Error(`${jailStorePath()} is not valid JSON \u2014 fix it before changing the jail.`);
|
|
6660
|
+
}
|
|
6661
|
+
if (!Array.isArray(parsed.paths)) return [];
|
|
6662
|
+
return parsed.paths.filter(
|
|
6663
|
+
(p) => !!p && typeof p.path === "string" && (p.verdict === "block" || p.verdict === "review")
|
|
6664
|
+
);
|
|
6665
|
+
}
|
|
6666
|
+
function findJailedPath(candidate) {
|
|
6667
|
+
return findJailedPathIn(candidate, readJailPaths());
|
|
6668
|
+
}
|
|
6669
|
+
function findJailedPathIn(candidate, paths) {
|
|
6670
|
+
if (!candidate) return null;
|
|
6671
|
+
for (const entry of paths) {
|
|
6672
|
+
if (pathMatchesFragment(candidate, entry.path)) return entry;
|
|
6673
|
+
}
|
|
6674
|
+
return null;
|
|
6675
|
+
}
|
|
6676
|
+
|
|
6677
|
+
// src/canary/registry.ts
|
|
6678
|
+
function canaryStorePath() {
|
|
6679
|
+
return import_path12.default.join(import_os9.default.homedir(), ".node9", "canaries.json");
|
|
6680
|
+
}
|
|
6681
|
+
function isRecord(x) {
|
|
6682
|
+
if (!x || typeof x !== "object") return false;
|
|
6683
|
+
const r = x;
|
|
6684
|
+
return typeof r.id === "string" && typeof r.value === "string" && typeof r.valueHash === "string";
|
|
6685
|
+
}
|
|
6686
|
+
function loadCanaries(opts) {
|
|
6687
|
+
const p = canaryStorePath();
|
|
6688
|
+
let raw;
|
|
6689
|
+
try {
|
|
6690
|
+
raw = import_fs10.default.readFileSync(p, "utf-8");
|
|
6691
|
+
} catch (e) {
|
|
6692
|
+
if (e.code === "ENOENT") return [];
|
|
6693
|
+
throw e;
|
|
6694
|
+
}
|
|
6695
|
+
let parsed;
|
|
6696
|
+
try {
|
|
6697
|
+
parsed = JSON.parse(raw);
|
|
6698
|
+
} catch (e) {
|
|
6699
|
+
throw new Error(
|
|
6700
|
+
`[node9] ${p} is not valid JSON; refusing to touch it (${e instanceof Error ? e.message : String(e)})`
|
|
6701
|
+
);
|
|
6702
|
+
}
|
|
6703
|
+
const recs = parsed?.records;
|
|
6704
|
+
if (!Array.isArray(recs)) return [];
|
|
6705
|
+
const out = recs.filter(isRecord);
|
|
6706
|
+
return opts?.includeRetired === false ? out.filter((r) => !r.retiredAt) : out;
|
|
6707
|
+
}
|
|
6708
|
+
function canaryValues() {
|
|
6709
|
+
return loadCanaries().map((r) => ({ id: r.id, value: r.value, retired: Boolean(r.retiredAt) }));
|
|
6710
|
+
}
|
|
6711
|
+
|
|
6712
|
+
// src/auth/orchestrator.ts
|
|
6713
|
+
init_audit();
|
|
6714
|
+
|
|
6715
|
+
// src/auth/cloud.ts
|
|
6716
|
+
var import_fs11 = __toESM(require("fs"));
|
|
6717
|
+
var import_os10 = __toESM(require("os"));
|
|
6718
|
+
var import_path13 = __toESM(require("path"));
|
|
5887
6719
|
init_audit();
|
|
5888
6720
|
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
|
|
5889
6721
|
const controller = new AbortController();
|
|
@@ -5892,10 +6724,10 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
5892
6724
|
let ciContext;
|
|
5893
6725
|
if (process.env.CI) {
|
|
5894
6726
|
try {
|
|
5895
|
-
const ciContextPath =
|
|
5896
|
-
const stats =
|
|
6727
|
+
const ciContextPath = import_path13.default.join(import_os10.default.homedir(), ".node9", "ci-context.json");
|
|
6728
|
+
const stats = import_fs11.default.statSync(ciContextPath);
|
|
5897
6729
|
if (stats.size > 1e4) throw new Error("ci-context.json exceeds 10 KB");
|
|
5898
|
-
const raw =
|
|
6730
|
+
const raw = import_fs11.default.readFileSync(ciContextPath, "utf8");
|
|
5899
6731
|
const parsed = JSON.parse(raw);
|
|
5900
6732
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
5901
6733
|
throw new Error("ci-context.json is not a plain object");
|
|
@@ -5928,9 +6760,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
5928
6760
|
context: {
|
|
5929
6761
|
agent: meta?.agent,
|
|
5930
6762
|
mcpServer: meta?.mcpServer,
|
|
5931
|
-
hostname:
|
|
6763
|
+
hostname: import_os10.default.hostname(),
|
|
5932
6764
|
cwd: process.cwd(),
|
|
5933
|
-
platform:
|
|
6765
|
+
platform: import_os10.default.platform()
|
|
5934
6766
|
},
|
|
5935
6767
|
...riskMetadata && { riskMetadata },
|
|
5936
6768
|
...ciContext && { ciContext },
|
|
@@ -5994,14 +6826,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
5994
6826
|
});
|
|
5995
6827
|
clearTimeout(timer);
|
|
5996
6828
|
if (!res.ok) {
|
|
5997
|
-
|
|
6829
|
+
import_fs11.default.appendFileSync(
|
|
5998
6830
|
HOOK_DEBUG_LOG,
|
|
5999
6831
|
`[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
|
|
6000
6832
|
`
|
|
6001
6833
|
);
|
|
6002
6834
|
}
|
|
6003
6835
|
} catch (err) {
|
|
6004
|
-
|
|
6836
|
+
import_fs11.default.appendFileSync(
|
|
6005
6837
|
HOOK_DEBUG_LOG,
|
|
6006
6838
|
`[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
|
|
6007
6839
|
`
|
|
@@ -6010,16 +6842,16 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
6010
6842
|
}
|
|
6011
6843
|
|
|
6012
6844
|
// src/loop-detector.ts
|
|
6013
|
-
var
|
|
6014
|
-
var
|
|
6015
|
-
var
|
|
6845
|
+
var import_fs12 = __toESM(require("fs"));
|
|
6846
|
+
var import_path14 = __toESM(require("path"));
|
|
6847
|
+
var import_os11 = __toESM(require("os"));
|
|
6016
6848
|
function loopStateFile() {
|
|
6017
|
-
return
|
|
6849
|
+
return import_path14.default.join(import_os11.default.homedir(), ".node9", "loop-state.json");
|
|
6018
6850
|
}
|
|
6019
6851
|
function readState() {
|
|
6020
6852
|
try {
|
|
6021
|
-
if (!
|
|
6022
|
-
const raw =
|
|
6853
|
+
if (!import_fs12.default.existsSync(loopStateFile())) return [];
|
|
6854
|
+
const raw = import_fs12.default.readFileSync(loopStateFile(), "utf-8");
|
|
6023
6855
|
const parsed = JSON.parse(raw);
|
|
6024
6856
|
if (!Array.isArray(parsed)) return [];
|
|
6025
6857
|
return parsed;
|
|
@@ -6028,11 +6860,11 @@ function readState() {
|
|
|
6028
6860
|
}
|
|
6029
6861
|
}
|
|
6030
6862
|
function writeState(records) {
|
|
6031
|
-
const dir =
|
|
6032
|
-
if (!
|
|
6033
|
-
const tmpPath = `${loopStateFile()}.${
|
|
6034
|
-
|
|
6035
|
-
|
|
6863
|
+
const dir = import_path14.default.dirname(loopStateFile());
|
|
6864
|
+
if (!import_fs12.default.existsSync(dir)) import_fs12.default.mkdirSync(dir, { recursive: true });
|
|
6865
|
+
const tmpPath = `${loopStateFile()}.${import_os11.default.hostname()}.${process.pid}.tmp`;
|
|
6866
|
+
import_fs12.default.writeFileSync(tmpPath, JSON.stringify(records));
|
|
6867
|
+
import_fs12.default.renameSync(tmpPath, loopStateFile());
|
|
6036
6868
|
}
|
|
6037
6869
|
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
6038
6870
|
try {
|
|
@@ -6044,44 +6876,6 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
|
6044
6876
|
}
|
|
6045
6877
|
}
|
|
6046
6878
|
|
|
6047
|
-
// src/shields/jail.ts
|
|
6048
|
-
var import_fs11 = __toESM(require("fs"));
|
|
6049
|
-
var import_os10 = __toESM(require("os"));
|
|
6050
|
-
var import_path13 = __toESM(require("path"));
|
|
6051
|
-
var USER_JAIL_SHIELD = "user-jail";
|
|
6052
|
-
function jailStorePath() {
|
|
6053
|
-
return import_path13.default.join(import_os10.default.homedir(), ".node9", "jail-paths.json");
|
|
6054
|
-
}
|
|
6055
|
-
function readJailPaths() {
|
|
6056
|
-
let text;
|
|
6057
|
-
try {
|
|
6058
|
-
text = import_fs11.default.readFileSync(jailStorePath(), "utf8");
|
|
6059
|
-
} catch (err) {
|
|
6060
|
-
if (err.code === "ENOENT") return [];
|
|
6061
|
-
throw err;
|
|
6062
|
-
}
|
|
6063
|
-
let parsed;
|
|
6064
|
-
try {
|
|
6065
|
-
parsed = JSON.parse(text);
|
|
6066
|
-
} catch {
|
|
6067
|
-
throw new Error(`${jailStorePath()} is not valid JSON \u2014 fix it before changing the jail.`);
|
|
6068
|
-
}
|
|
6069
|
-
if (!Array.isArray(parsed.paths)) return [];
|
|
6070
|
-
return parsed.paths.filter(
|
|
6071
|
-
(p) => !!p && typeof p.path === "string" && (p.verdict === "block" || p.verdict === "review")
|
|
6072
|
-
);
|
|
6073
|
-
}
|
|
6074
|
-
function findJailedPath(candidate) {
|
|
6075
|
-
return findJailedPathIn(candidate, readJailPaths());
|
|
6076
|
-
}
|
|
6077
|
-
function findJailedPathIn(candidate, paths) {
|
|
6078
|
-
if (!candidate) return null;
|
|
6079
|
-
for (const entry of paths) {
|
|
6080
|
-
if (pathMatchesFragment(candidate, entry.path)) return entry;
|
|
6081
|
-
}
|
|
6082
|
-
return null;
|
|
6083
|
-
}
|
|
6084
|
-
|
|
6085
6879
|
// src/auth/orchestrator.ts
|
|
6086
6880
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
6087
6881
|
"write",
|
|
@@ -6139,7 +6933,7 @@ async function hasReachableHumanApprover(opts) {
|
|
|
6139
6933
|
}
|
|
6140
6934
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
6141
6935
|
if (!options?.calledFromDaemon) {
|
|
6142
|
-
const actId = (0,
|
|
6936
|
+
const actId = (0, import_crypto5.randomUUID)();
|
|
6143
6937
|
const actTs = Date.now();
|
|
6144
6938
|
const stripAnsi = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
|
|
6145
6939
|
const sanitizedAgent = meta?.agent ? stripAnsi(meta.agent).slice(0, 80) : void 0;
|
|
@@ -6267,6 +7061,53 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
6267
7061
|
taintWarning = `\u26A0\uFE0F node9 flagged this session \u2014 earlier tool output contained ${sessionTaint.record.source}. Approve this ${isWriteTool(toolName) ? "write" : "network"} action before it proceeds.`;
|
|
6268
7062
|
}
|
|
6269
7063
|
}
|
|
7064
|
+
const canaryVals = safeCanaryValues();
|
|
7065
|
+
if (canaryVals.length > 0) {
|
|
7066
|
+
const canaryHit = matchCanaryArgs(args, canaryVals);
|
|
7067
|
+
if (canaryHit) {
|
|
7068
|
+
const rec = canaryRecordById(canaryHit.id);
|
|
7069
|
+
const shape = scanArgs(args);
|
|
7070
|
+
const canaryReason = `\u{1F6A8} DECOY CREDENTIAL: the fake ${rec?.kind ?? "credential"} node9 planted at ${rec?.path ?? "a decoy file"} appeared in field "${canaryHit.fieldPath || "args"}". Something read that file; nothing legitimate does.`;
|
|
7071
|
+
if (!isManual)
|
|
7072
|
+
appendLocalAudit(
|
|
7073
|
+
toolName,
|
|
7074
|
+
args,
|
|
7075
|
+
"deny",
|
|
7076
|
+
isObserveMode ? "observe-mode-dlp-canary-would-block" : "dlp-canary-block",
|
|
7077
|
+
{
|
|
7078
|
+
...meta,
|
|
7079
|
+
canaryId: canaryHit.id,
|
|
7080
|
+
canaryHash: rec?.valueHash,
|
|
7081
|
+
canaryKind: rec?.kind,
|
|
7082
|
+
canaryPath: rec?.path,
|
|
7083
|
+
canaryView: canaryHit.view,
|
|
7084
|
+
canaryRetired: canaryHit.retired,
|
|
7085
|
+
...shape ? { dlpPattern: shape.patternName, dlpSample: shape.redactedSample } : {}
|
|
7086
|
+
},
|
|
7087
|
+
true
|
|
7088
|
+
);
|
|
7089
|
+
if (isObserveMode) {
|
|
7090
|
+
return {
|
|
7091
|
+
approved: true,
|
|
7092
|
+
checkedBy: "audit",
|
|
7093
|
+
observeWouldBlock: true,
|
|
7094
|
+
blockedByLabel: "\u{1F6A8} Node9 DLP (Decoy Credential)"
|
|
7095
|
+
};
|
|
7096
|
+
}
|
|
7097
|
+
return {
|
|
7098
|
+
approved: false,
|
|
7099
|
+
reason: canaryReason,
|
|
7100
|
+
blockedBy: "local-config",
|
|
7101
|
+
blockedByLabel: "\u{1F6A8} Node9 DLP (Decoy Credential)",
|
|
7102
|
+
// The /dev/tty banner renders ruleDescription under "Triggered by".
|
|
7103
|
+
// Without it the terminal said only "Decoy Credential" and never named
|
|
7104
|
+
// the file, while `node9 canary plant` promises node9 tells you which
|
|
7105
|
+
// file was read. Witnessed by canary-block-message.spec.ts, which calls
|
|
7106
|
+
// the orchestrator directly: this field never reaches the hook stdout.
|
|
7107
|
+
ruleDescription: `The fake credential node9 planted in ${rec?.path ?? "a decoy file"} just left that file. Nothing legitimate reads it.`
|
|
7108
|
+
};
|
|
7109
|
+
}
|
|
7110
|
+
}
|
|
6270
7111
|
if (config.policy.dlp.enabled && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
6271
7112
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
6272
7113
|
const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
|
|
@@ -6922,6 +7763,20 @@ async function authorizeAction(toolName, args) {
|
|
|
6922
7763
|
const result = await authorizeHeadless(toolName, args);
|
|
6923
7764
|
return result.approved;
|
|
6924
7765
|
}
|
|
7766
|
+
function safeCanaryValues() {
|
|
7767
|
+
try {
|
|
7768
|
+
return canaryValues();
|
|
7769
|
+
} catch {
|
|
7770
|
+
return [];
|
|
7771
|
+
}
|
|
7772
|
+
}
|
|
7773
|
+
function canaryRecordById(id) {
|
|
7774
|
+
try {
|
|
7775
|
+
return loadCanaries().find((r) => r.id === id) ?? null;
|
|
7776
|
+
} catch {
|
|
7777
|
+
return null;
|
|
7778
|
+
}
|
|
7779
|
+
}
|
|
6925
7780
|
|
|
6926
7781
|
// src/index.ts
|
|
6927
7782
|
function protect(toolName, fn) {
|