@node9/proxy 2.8.4 → 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 +3775 -1865
- package/dist/cli.mjs +3711 -1801
- package/dist/dashboard.mjs +854 -165
- package/dist/index.js +972 -113
- package/dist/index.mjs +968 -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();
|
|
@@ -2274,6 +2544,10 @@ function analyzeShellCommand(command) {
|
|
|
2274
2544
|
return { actions, paths, allTokens };
|
|
2275
2545
|
}
|
|
2276
2546
|
var DEFAULT_EGRESS_ALLOWLIST = [
|
|
2547
|
+
// node9's own control plane (api, app, dev-api, staging and the apex).
|
|
2548
|
+
// Without it, turning egress on asks the user to approve node9 itself.
|
|
2549
|
+
// A user `deny` entry still wins over this list, see evaluateEgress.
|
|
2550
|
+
"*.node9.ai",
|
|
2277
2551
|
"*.github.com",
|
|
2278
2552
|
"*.githubusercontent.com",
|
|
2279
2553
|
"*.npmjs.org",
|
|
@@ -2416,7 +2690,7 @@ function isSensitivePath(p) {
|
|
|
2416
2690
|
return SENSITIVE_PATTERNS.some((re) => re.test(p));
|
|
2417
2691
|
}
|
|
2418
2692
|
function splitOnPipe(cmd) {
|
|
2419
|
-
const
|
|
2693
|
+
const segments2 = [];
|
|
2420
2694
|
let current = "";
|
|
2421
2695
|
let inSingle = false;
|
|
2422
2696
|
let inDouble = false;
|
|
@@ -2429,21 +2703,21 @@ function splitOnPipe(cmd) {
|
|
|
2429
2703
|
inDouble = !inDouble;
|
|
2430
2704
|
current += ch;
|
|
2431
2705
|
} else if (ch === "|" && !inSingle && !inDouble && cmd[i + 1] !== "|" && (i === 0 || cmd[i - 1] !== "|")) {
|
|
2432
|
-
|
|
2706
|
+
segments2.push(current.trim());
|
|
2433
2707
|
current = "";
|
|
2434
2708
|
} else {
|
|
2435
2709
|
current += ch;
|
|
2436
2710
|
}
|
|
2437
2711
|
}
|
|
2438
|
-
if (current.trim())
|
|
2439
|
-
return
|
|
2712
|
+
if (current.trim()) segments2.push(current.trim());
|
|
2713
|
+
return segments2.filter(Boolean);
|
|
2440
2714
|
}
|
|
2441
2715
|
function positionalTokens(segment) {
|
|
2442
2716
|
return segment.split(/\s+/).slice(1).filter((t) => !t.startsWith("-") && !t.startsWith("@") && t.length > 0);
|
|
2443
2717
|
}
|
|
2444
2718
|
function analyzePipeChain(command) {
|
|
2445
|
-
const
|
|
2446
|
-
if (
|
|
2719
|
+
const segments2 = splitOnPipe(command);
|
|
2720
|
+
if (segments2.length < 2) {
|
|
2447
2721
|
return {
|
|
2448
2722
|
isPipeline: false,
|
|
2449
2723
|
hasSensitiveSource: false,
|
|
@@ -2459,7 +2733,7 @@ function analyzePipeChain(command) {
|
|
|
2459
2733
|
let hasSensitiveSource = false;
|
|
2460
2734
|
let hasExternalSink = false;
|
|
2461
2735
|
let hasObfuscation = false;
|
|
2462
|
-
for (const segment of
|
|
2736
|
+
for (const segment of segments2) {
|
|
2463
2737
|
const tokens = segment.split(/\s+/).filter(Boolean);
|
|
2464
2738
|
if (tokens.length === 0) continue;
|
|
2465
2739
|
const binary = tokens[0].toLowerCase();
|
|
@@ -2497,8 +2771,8 @@ function analyzePipeChain(command) {
|
|
|
2497
2771
|
};
|
|
2498
2772
|
}
|
|
2499
2773
|
function basename(p) {
|
|
2500
|
-
const
|
|
2501
|
-
return
|
|
2774
|
+
const segments2 = p.split(/[\\/]/);
|
|
2775
|
+
return segments2[segments2.length - 1] || "";
|
|
2502
2776
|
}
|
|
2503
2777
|
var FLAGS_WITH_VALUES = {
|
|
2504
2778
|
curl: /* @__PURE__ */ new Set([
|
|
@@ -2661,6 +2935,204 @@ function extractAllSshHosts(tokens) {
|
|
|
2661
2935
|
}
|
|
2662
2936
|
return [...hosts].filter(Boolean);
|
|
2663
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
|
+
}
|
|
2664
3136
|
function resolveCheck(v) {
|
|
2665
3137
|
return v === "off" || v === "block" ? v : "review";
|
|
2666
3138
|
}
|
|
@@ -2893,6 +3365,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2893
3365
|
}
|
|
2894
3366
|
const builtin = strictestVerdict(candidates);
|
|
2895
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
|
+
}
|
|
2896
3384
|
if (config.policy.egress?.enabled) {
|
|
2897
3385
|
const dests = extractShellDestinations(shellCommand);
|
|
2898
3386
|
if (dests.length > 0) {
|
|
@@ -3820,7 +4308,7 @@ assertBuiltinShieldRegexesAreSafe();
|
|
|
3820
4308
|
var LOOP_MAX_RECORDS = 500;
|
|
3821
4309
|
function computeArgsHash(args) {
|
|
3822
4310
|
const str = JSON.stringify(args ?? "");
|
|
3823
|
-
return
|
|
4311
|
+
return import_crypto4.default.createHash("sha256").update(str).digest("hex").slice(0, 16);
|
|
3824
4312
|
}
|
|
3825
4313
|
function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
|
|
3826
4314
|
const hash = computeArgsHash(args);
|
|
@@ -3834,28 +4322,236 @@ function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
|
|
|
3834
4322
|
var PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
3835
4323
|
var PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
3836
4324
|
var PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
|
|
3837
|
-
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
|
+
}
|
|
3838
4348
|
function detectPii(text) {
|
|
3839
4349
|
const found = /* @__PURE__ */ new Set();
|
|
3840
4350
|
if (/@/.test(text) && PII_EMAIL_RE.test(text)) found.add("Email");
|
|
3841
4351
|
if (/-/.test(text) && PII_SSN_RE.test(text)) found.add("SSN");
|
|
3842
4352
|
if (PII_PHONE_RE.test(text)) found.add("Phone");
|
|
3843
|
-
if (
|
|
4353
|
+
if (hasValidCard(text)) found.add("Credit Card");
|
|
4354
|
+
if (hasValidIban(text)) found.add("IBAN");
|
|
3844
4355
|
return [...found];
|
|
3845
4356
|
}
|
|
3846
|
-
var REALTIME_PII_PATTERNS = ["SSN", "Credit Card"];
|
|
4357
|
+
var REALTIME_PII_PATTERNS = ["SSN", "Credit Card", "IBAN"];
|
|
3847
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
|
+
}
|
|
3848
4376
|
function detectArgsPii(args) {
|
|
3849
4377
|
if (args === null || args === void 0) return [];
|
|
3850
|
-
|
|
4378
|
+
const found = /* @__PURE__ */ new Set();
|
|
4379
|
+
let budget = MAX_PII_SCAN_BYTES;
|
|
3851
4380
|
try {
|
|
3852
|
-
|
|
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
|
+
}
|
|
3853
4389
|
} catch {
|
|
3854
4390
|
return [];
|
|
3855
4391
|
}
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
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
|
+
}
|
|
3859
4555
|
}
|
|
3860
4556
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
3861
4557
|
|
|
@@ -3985,6 +4681,12 @@ function applyManagedEgress(local, managed, locked, localModeUserSet = true) {
|
|
|
3985
4681
|
if (Array.isArray(managed.deny) && managed.deny.length > 0) {
|
|
3986
4682
|
next.deny = [.../* @__PURE__ */ new Set([...local.deny ?? [], ...managed.deny])];
|
|
3987
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
|
+
}
|
|
3988
4690
|
if (typeof managed.allowPrivate === "boolean") {
|
|
3989
4691
|
next.allowPrivate = locked.includes("egressAllowPrivate") ? managed.allowPrivate : (local.allowPrivate ?? true) && managed.allowPrivate;
|
|
3990
4692
|
}
|
|
@@ -4066,9 +4768,9 @@ var B = "[\\s/\\\\]";
|
|
|
4066
4768
|
var SEP = "[/\\\\]";
|
|
4067
4769
|
function pathToRegexFragment(rawPath) {
|
|
4068
4770
|
const tail = rawPath.trim().replace(/^~[\\/]?/, "").replace(/^\$\{?HOME\}?[\\/]?/, "").replace(/^\/(?:home|Users)\/[^\\/]+[\\/]?/, "").replace(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+[\\/]?/, "").replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
|
|
4069
|
-
const
|
|
4070
|
-
if (
|
|
4071
|
-
return `(^|${B})${
|
|
4771
|
+
const segments2 = tail.split(/[\\/]+/).filter(Boolean).map(escapeRegex);
|
|
4772
|
+
if (segments2.length === 0) return "";
|
|
4773
|
+
return `(^|${B})${segments2.join(SEP)}(${B}|$)`;
|
|
4072
4774
|
}
|
|
4073
4775
|
function pathMatchesFragment(candidate, rawPath) {
|
|
4074
4776
|
const value = pathToRegexFragment(rawPath);
|
|
@@ -4173,6 +4875,20 @@ function isTrustedHost(host) {
|
|
|
4173
4875
|
}
|
|
4174
4876
|
|
|
4175
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
|
+
}
|
|
4176
4892
|
var DANGEROUS_WORDS = [
|
|
4177
4893
|
"mkfs",
|
|
4178
4894
|
// formats/wipes a filesystem partition
|
|
@@ -4182,6 +4898,7 @@ var DANGEROUS_WORDS = [
|
|
|
4182
4898
|
var DEFAULT_CONFIG = {
|
|
4183
4899
|
version: "1.0",
|
|
4184
4900
|
policySource: "local",
|
|
4901
|
+
ssrfStrictSource: "default",
|
|
4185
4902
|
settings: {
|
|
4186
4903
|
mode: "standard",
|
|
4187
4904
|
autoStartDaemon: true,
|
|
@@ -4365,7 +5082,17 @@ var DEFAULT_CONFIG = {
|
|
|
4365
5082
|
}
|
|
4366
5083
|
],
|
|
4367
5084
|
dlp: { enabled: true, scanIgnoredTools: true, pii: "off" },
|
|
4368
|
-
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
|
+
},
|
|
4369
5096
|
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
|
|
4370
5097
|
injectionScan: { enabled: false, minConfidence: "medium", allow: [] },
|
|
4371
5098
|
skillPinning: { enabled: false, mode: "warn", roots: [] },
|
|
@@ -4548,7 +5275,8 @@ function getConfig(cwd) {
|
|
|
4548
5275
|
egress: {
|
|
4549
5276
|
...DEFAULT_CONFIG.policy.egress,
|
|
4550
5277
|
allow: [...DEFAULT_CONFIG.policy.egress.allow],
|
|
4551
|
-
deny: [...DEFAULT_CONFIG.policy.egress.deny]
|
|
5278
|
+
deny: [...DEFAULT_CONFIG.policy.egress.deny],
|
|
5279
|
+
ssrfAllow: [...DEFAULT_CONFIG.policy.egress.ssrfAllow ?? []]
|
|
4552
5280
|
},
|
|
4553
5281
|
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
|
|
4554
5282
|
injectionScan: {
|
|
@@ -4574,6 +5302,7 @@ function getConfig(cwd) {
|
|
|
4574
5302
|
};
|
|
4575
5303
|
const pr2Creds = getCredentials();
|
|
4576
5304
|
const keyed = !!pr2Creds?.apiKey && pr2Creds.localOnly !== true;
|
|
5305
|
+
let ssrfStrictSource = "default";
|
|
4577
5306
|
const applyLayer = (source, isProject = false, isCloud = false) => {
|
|
4578
5307
|
if (!source) return;
|
|
4579
5308
|
const s = source.settings || {};
|
|
@@ -4656,6 +5385,15 @@ function getConfig(cwd) {
|
|
|
4656
5385
|
if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
|
|
4657
5386
|
if (e.allowPrivate !== void 0 && !(isProject && e.allowPrivate === true))
|
|
4658
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
|
+
}
|
|
4659
5397
|
}
|
|
4660
5398
|
if (p.loopDetection) {
|
|
4661
5399
|
const ld = p.loopDetection;
|
|
@@ -4762,6 +5500,13 @@ function getConfig(cwd) {
|
|
|
4762
5500
|
if (deny) mergedPolicy.egress.deny = deny;
|
|
4763
5501
|
if (typeof e.allowPrivate === "boolean")
|
|
4764
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.");
|
|
4765
5510
|
} else {
|
|
4766
5511
|
mergedPolicy.egress = applyManagedEgress(
|
|
4767
5512
|
mergedPolicy.egress,
|
|
@@ -4770,7 +5515,16 @@ function getConfig(cwd) {
|
|
|
4770
5515
|
mode: typeof mc.egress.mode === "string" ? mc.egress.mode : void 0,
|
|
4771
5516
|
allow: hosts(mc.egress.allow),
|
|
4772
5517
|
deny: hosts(mc.egress.deny),
|
|
4773
|
-
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
|
+
})()
|
|
4774
5528
|
},
|
|
4775
5529
|
locked,
|
|
4776
5530
|
egressModeUserSet
|
|
@@ -4865,13 +5619,13 @@ function getConfig(cwd) {
|
|
|
4865
5619
|
}
|
|
4866
5620
|
if (Array.isArray(mc.jailPaths)) {
|
|
4867
5621
|
for (const jp of mc.jailPaths) {
|
|
4868
|
-
const
|
|
4869
|
-
if (!
|
|
5622
|
+
const path15 = typeof jp?.path === "string" ? jp.path.trim() : "";
|
|
5623
|
+
if (!path15) continue;
|
|
4870
5624
|
const verdict = jp?.verdict === "review" ? "review" : "block";
|
|
4871
|
-
for (const r of pathRules(
|
|
5625
|
+
for (const r of pathRules(path15, verdict, "org-managed jail")) {
|
|
4872
5626
|
mergedPolicy.smartRules.push({ ...r, name: `org:${r.name}` });
|
|
4873
5627
|
}
|
|
4874
|
-
mergedPolicy.managedJailPaths.push({ path:
|
|
5628
|
+
mergedPolicy.managedJailPaths.push({ path: path15, verdict });
|
|
4875
5629
|
}
|
|
4876
5630
|
}
|
|
4877
5631
|
if (Array.isArray(mc.trustedHosts)) {
|
|
@@ -5000,6 +5754,7 @@ function getConfig(cwd) {
|
|
|
5000
5754
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
5001
5755
|
mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
|
|
5002
5756
|
mergedPolicy.skillPinning.roots = [...new Set(mergedPolicy.skillPinning.roots)];
|
|
5757
|
+
const resolvedSsrfStrictSource = keyed && ssrfStrictSource === "local" ? "default" : ssrfStrictSource;
|
|
5003
5758
|
const result = {
|
|
5004
5759
|
settings: mergedSettings,
|
|
5005
5760
|
policy: mergedPolicy,
|
|
@@ -5007,7 +5762,10 @@ function getConfig(cwd) {
|
|
|
5007
5762
|
// PR-2 — the one truth introspection reads: which of the two working
|
|
5008
5763
|
// modes this machine is in. 'workspace' = keyed, policy from the cloud;
|
|
5009
5764
|
// 'local' = the local stack (incl. --local / named-profile keys).
|
|
5010
|
-
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
|
|
5011
5769
|
};
|
|
5012
5770
|
if (!cwd) cachedConfig = result;
|
|
5013
5771
|
return result;
|
|
@@ -5537,7 +6295,7 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
5537
6295
|
}
|
|
5538
6296
|
|
|
5539
6297
|
// src/auth/orchestrator.ts
|
|
5540
|
-
var
|
|
6298
|
+
var import_crypto5 = require("crypto");
|
|
5541
6299
|
|
|
5542
6300
|
// src/ui/native.ts
|
|
5543
6301
|
var import_child_process = require("child_process");
|
|
@@ -5873,13 +6631,91 @@ end run`;
|
|
|
5873
6631
|
});
|
|
5874
6632
|
}
|
|
5875
6633
|
|
|
5876
|
-
// src/
|
|
5877
|
-
|
|
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"));
|
|
5878
6638
|
|
|
5879
|
-
// src/
|
|
6639
|
+
// src/shields/jail.ts
|
|
5880
6640
|
var import_fs9 = __toESM(require("fs"));
|
|
5881
6641
|
var import_os8 = __toESM(require("os"));
|
|
5882
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"));
|
|
5883
6719
|
init_audit();
|
|
5884
6720
|
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
|
|
5885
6721
|
const controller = new AbortController();
|
|
@@ -5888,10 +6724,10 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
5888
6724
|
let ciContext;
|
|
5889
6725
|
if (process.env.CI) {
|
|
5890
6726
|
try {
|
|
5891
|
-
const ciContextPath =
|
|
5892
|
-
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);
|
|
5893
6729
|
if (stats.size > 1e4) throw new Error("ci-context.json exceeds 10 KB");
|
|
5894
|
-
const raw =
|
|
6730
|
+
const raw = import_fs11.default.readFileSync(ciContextPath, "utf8");
|
|
5895
6731
|
const parsed = JSON.parse(raw);
|
|
5896
6732
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
5897
6733
|
throw new Error("ci-context.json is not a plain object");
|
|
@@ -5924,9 +6760,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
5924
6760
|
context: {
|
|
5925
6761
|
agent: meta?.agent,
|
|
5926
6762
|
mcpServer: meta?.mcpServer,
|
|
5927
|
-
hostname:
|
|
6763
|
+
hostname: import_os10.default.hostname(),
|
|
5928
6764
|
cwd: process.cwd(),
|
|
5929
|
-
platform:
|
|
6765
|
+
platform: import_os10.default.platform()
|
|
5930
6766
|
},
|
|
5931
6767
|
...riskMetadata && { riskMetadata },
|
|
5932
6768
|
...ciContext && { ciContext },
|
|
@@ -5990,14 +6826,14 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
5990
6826
|
});
|
|
5991
6827
|
clearTimeout(timer);
|
|
5992
6828
|
if (!res.ok) {
|
|
5993
|
-
|
|
6829
|
+
import_fs11.default.appendFileSync(
|
|
5994
6830
|
HOOK_DEBUG_LOG,
|
|
5995
6831
|
`[resolve-cloud] PATCH ${resolveUrl} \u2192 HTTP ${res.status}
|
|
5996
6832
|
`
|
|
5997
6833
|
);
|
|
5998
6834
|
}
|
|
5999
6835
|
} catch (err) {
|
|
6000
|
-
|
|
6836
|
+
import_fs11.default.appendFileSync(
|
|
6001
6837
|
HOOK_DEBUG_LOG,
|
|
6002
6838
|
`[resolve-cloud] PATCH failed for ${requestId}: ${err.message}
|
|
6003
6839
|
`
|
|
@@ -6006,16 +6842,16 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
6006
6842
|
}
|
|
6007
6843
|
|
|
6008
6844
|
// src/loop-detector.ts
|
|
6009
|
-
var
|
|
6010
|
-
var
|
|
6011
|
-
var
|
|
6845
|
+
var import_fs12 = __toESM(require("fs"));
|
|
6846
|
+
var import_path14 = __toESM(require("path"));
|
|
6847
|
+
var import_os11 = __toESM(require("os"));
|
|
6012
6848
|
function loopStateFile() {
|
|
6013
|
-
return
|
|
6849
|
+
return import_path14.default.join(import_os11.default.homedir(), ".node9", "loop-state.json");
|
|
6014
6850
|
}
|
|
6015
6851
|
function readState() {
|
|
6016
6852
|
try {
|
|
6017
|
-
if (!
|
|
6018
|
-
const raw =
|
|
6853
|
+
if (!import_fs12.default.existsSync(loopStateFile())) return [];
|
|
6854
|
+
const raw = import_fs12.default.readFileSync(loopStateFile(), "utf-8");
|
|
6019
6855
|
const parsed = JSON.parse(raw);
|
|
6020
6856
|
if (!Array.isArray(parsed)) return [];
|
|
6021
6857
|
return parsed;
|
|
@@ -6024,11 +6860,11 @@ function readState() {
|
|
|
6024
6860
|
}
|
|
6025
6861
|
}
|
|
6026
6862
|
function writeState(records) {
|
|
6027
|
-
const dir =
|
|
6028
|
-
if (!
|
|
6029
|
-
const tmpPath = `${loopStateFile()}.${
|
|
6030
|
-
|
|
6031
|
-
|
|
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());
|
|
6032
6868
|
}
|
|
6033
6869
|
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
6034
6870
|
try {
|
|
@@ -6040,44 +6876,6 @@ function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
|
6040
6876
|
}
|
|
6041
6877
|
}
|
|
6042
6878
|
|
|
6043
|
-
// src/shields/jail.ts
|
|
6044
|
-
var import_fs11 = __toESM(require("fs"));
|
|
6045
|
-
var import_os10 = __toESM(require("os"));
|
|
6046
|
-
var import_path13 = __toESM(require("path"));
|
|
6047
|
-
var USER_JAIL_SHIELD = "user-jail";
|
|
6048
|
-
function jailStorePath() {
|
|
6049
|
-
return import_path13.default.join(import_os10.default.homedir(), ".node9", "jail-paths.json");
|
|
6050
|
-
}
|
|
6051
|
-
function readJailPaths() {
|
|
6052
|
-
let text;
|
|
6053
|
-
try {
|
|
6054
|
-
text = import_fs11.default.readFileSync(jailStorePath(), "utf8");
|
|
6055
|
-
} catch (err) {
|
|
6056
|
-
if (err.code === "ENOENT") return [];
|
|
6057
|
-
throw err;
|
|
6058
|
-
}
|
|
6059
|
-
let parsed;
|
|
6060
|
-
try {
|
|
6061
|
-
parsed = JSON.parse(text);
|
|
6062
|
-
} catch {
|
|
6063
|
-
throw new Error(`${jailStorePath()} is not valid JSON \u2014 fix it before changing the jail.`);
|
|
6064
|
-
}
|
|
6065
|
-
if (!Array.isArray(parsed.paths)) return [];
|
|
6066
|
-
return parsed.paths.filter(
|
|
6067
|
-
(p) => !!p && typeof p.path === "string" && (p.verdict === "block" || p.verdict === "review")
|
|
6068
|
-
);
|
|
6069
|
-
}
|
|
6070
|
-
function findJailedPath(candidate) {
|
|
6071
|
-
return findJailedPathIn(candidate, readJailPaths());
|
|
6072
|
-
}
|
|
6073
|
-
function findJailedPathIn(candidate, paths) {
|
|
6074
|
-
if (!candidate) return null;
|
|
6075
|
-
for (const entry of paths) {
|
|
6076
|
-
if (pathMatchesFragment(candidate, entry.path)) return entry;
|
|
6077
|
-
}
|
|
6078
|
-
return null;
|
|
6079
|
-
}
|
|
6080
|
-
|
|
6081
6879
|
// src/auth/orchestrator.ts
|
|
6082
6880
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
6083
6881
|
"write",
|
|
@@ -6135,7 +6933,7 @@ async function hasReachableHumanApprover(opts) {
|
|
|
6135
6933
|
}
|
|
6136
6934
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
6137
6935
|
if (!options?.calledFromDaemon) {
|
|
6138
|
-
const actId = (0,
|
|
6936
|
+
const actId = (0, import_crypto5.randomUUID)();
|
|
6139
6937
|
const actTs = Date.now();
|
|
6140
6938
|
const stripAnsi = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
|
|
6141
6939
|
const sanitizedAgent = meta?.agent ? stripAnsi(meta.agent).slice(0, 80) : void 0;
|
|
@@ -6263,6 +7061,53 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
6263
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.`;
|
|
6264
7062
|
}
|
|
6265
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
|
+
}
|
|
6266
7111
|
if (config.policy.dlp.enabled && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
6267
7112
|
const argsObj = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
6268
7113
|
const filePath = String(argsObj.file_path ?? argsObj.path ?? argsObj.filename ?? "");
|
|
@@ -6918,6 +7763,20 @@ async function authorizeAction(toolName, args) {
|
|
|
6918
7763
|
const result = await authorizeHeadless(toolName, args);
|
|
6919
7764
|
return result.approved;
|
|
6920
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
|
+
}
|
|
6921
7780
|
|
|
6922
7781
|
// src/index.ts
|
|
6923
7782
|
function protect(toolName, fn) {
|