@node9/proxy 1.30.0 → 1.31.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 +1314 -764
- package/dist/cli.mjs +1299 -749
- package/dist/dashboard.mjs +20 -2
- package/dist/index.js +414 -6
- package/dist/index.mjs +414 -6
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -263,7 +263,15 @@ var ConfigFileSchema = z.object({
|
|
|
263
263
|
}).optional(),
|
|
264
264
|
dlp: z.object({
|
|
265
265
|
enabled: z.boolean().optional(),
|
|
266
|
-
scanIgnoredTools: z.boolean().optional()
|
|
266
|
+
scanIgnoredTools: z.boolean().optional(),
|
|
267
|
+
pii: z.enum(["off", "block"]).optional()
|
|
268
|
+
}).optional(),
|
|
269
|
+
egress: z.object({
|
|
270
|
+
enabled: z.boolean().optional(),
|
|
271
|
+
mode: z.enum(["off", "review", "block"]).optional(),
|
|
272
|
+
allow: z.array(z.string()).optional(),
|
|
273
|
+
deny: z.array(z.string()).optional(),
|
|
274
|
+
allowPrivate: z.boolean().optional()
|
|
267
275
|
}).optional(),
|
|
268
276
|
loopDetection: z.object({
|
|
269
277
|
enabled: z.boolean().optional(),
|
|
@@ -1245,6 +1253,201 @@ function extractLiteralArgs(callExpr) {
|
|
|
1245
1253
|
}
|
|
1246
1254
|
return { name, flags, paths };
|
|
1247
1255
|
}
|
|
1256
|
+
var NET_BINARIES = /* @__PURE__ */ new Set(["curl", "wget", "scp", "ssh", "nc", "ncat", "netcat"]);
|
|
1257
|
+
var VALUE_FLAGS = {
|
|
1258
|
+
curl: /* @__PURE__ */ new Set([
|
|
1259
|
+
"-d",
|
|
1260
|
+
"--data",
|
|
1261
|
+
"--data-ascii",
|
|
1262
|
+
"--data-binary",
|
|
1263
|
+
"--data-raw",
|
|
1264
|
+
"--data-urlencode",
|
|
1265
|
+
"-F",
|
|
1266
|
+
"--form",
|
|
1267
|
+
"-H",
|
|
1268
|
+
"--header",
|
|
1269
|
+
"-X",
|
|
1270
|
+
"--request",
|
|
1271
|
+
"-o",
|
|
1272
|
+
"--output",
|
|
1273
|
+
"-T",
|
|
1274
|
+
"--upload-file",
|
|
1275
|
+
"-u",
|
|
1276
|
+
"--user",
|
|
1277
|
+
"-e",
|
|
1278
|
+
"--referer",
|
|
1279
|
+
"-A",
|
|
1280
|
+
"--user-agent",
|
|
1281
|
+
"-b",
|
|
1282
|
+
"--cookie",
|
|
1283
|
+
"-c",
|
|
1284
|
+
"--cookie-jar",
|
|
1285
|
+
"--connect-to",
|
|
1286
|
+
"--resolve",
|
|
1287
|
+
"--cacert",
|
|
1288
|
+
"--cert",
|
|
1289
|
+
"--key",
|
|
1290
|
+
"-x",
|
|
1291
|
+
"--proxy",
|
|
1292
|
+
"-m",
|
|
1293
|
+
"--max-time",
|
|
1294
|
+
"--retry"
|
|
1295
|
+
]),
|
|
1296
|
+
wget: /* @__PURE__ */ new Set([
|
|
1297
|
+
"-O",
|
|
1298
|
+
"--output-document",
|
|
1299
|
+
"--post-data",
|
|
1300
|
+
"--post-file",
|
|
1301
|
+
"--header",
|
|
1302
|
+
"-U",
|
|
1303
|
+
"--user-agent",
|
|
1304
|
+
"--user",
|
|
1305
|
+
"--password",
|
|
1306
|
+
"-o",
|
|
1307
|
+
"--output-file",
|
|
1308
|
+
"-P",
|
|
1309
|
+
"--directory-prefix",
|
|
1310
|
+
"-t",
|
|
1311
|
+
"--tries",
|
|
1312
|
+
"-T",
|
|
1313
|
+
"--timeout"
|
|
1314
|
+
]),
|
|
1315
|
+
scp: /* @__PURE__ */ new Set(["-i", "-F", "-l", "-o", "-c", "-S", "-P", "-J", "-D", "-W"]),
|
|
1316
|
+
ssh: /* @__PURE__ */ new Set([
|
|
1317
|
+
"-i",
|
|
1318
|
+
"-p",
|
|
1319
|
+
"-o",
|
|
1320
|
+
"-l",
|
|
1321
|
+
"-F",
|
|
1322
|
+
"-c",
|
|
1323
|
+
"-L",
|
|
1324
|
+
"-R",
|
|
1325
|
+
"-D",
|
|
1326
|
+
"-W",
|
|
1327
|
+
"-b",
|
|
1328
|
+
"-e",
|
|
1329
|
+
"-m",
|
|
1330
|
+
"-O",
|
|
1331
|
+
"-Q",
|
|
1332
|
+
"-S",
|
|
1333
|
+
"-J",
|
|
1334
|
+
"-w",
|
|
1335
|
+
"-B",
|
|
1336
|
+
"-I",
|
|
1337
|
+
"-E"
|
|
1338
|
+
]),
|
|
1339
|
+
nc: /* @__PURE__ */ new Set(["-p", "-s", "-w", "-X", "-x", "-e", "-g", "-G", "-i", "-O", "-T", "-q", "-m"])
|
|
1340
|
+
};
|
|
1341
|
+
function resolveWordLiteral(w) {
|
|
1342
|
+
const parts = w?.Parts || [];
|
|
1343
|
+
let s = "";
|
|
1344
|
+
for (const p of parts) {
|
|
1345
|
+
const t = syntax.NodeType(p);
|
|
1346
|
+
if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
|
|
1347
|
+
else if (t === "SglQuoted") s += p.Value ?? "";
|
|
1348
|
+
else if (t === "DblQuoted") {
|
|
1349
|
+
const inner = p.Parts || [];
|
|
1350
|
+
if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
|
|
1351
|
+
s += inner.map((ip) => ip.Value ?? "").join("");
|
|
1352
|
+
} else {
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return s;
|
|
1357
|
+
}
|
|
1358
|
+
function parseDestHost(token) {
|
|
1359
|
+
if (!token) return null;
|
|
1360
|
+
let t = token.trim();
|
|
1361
|
+
if (!t || t.startsWith("-")) return null;
|
|
1362
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(t)) {
|
|
1363
|
+
try {
|
|
1364
|
+
const h = new URL(t).hostname.toLowerCase();
|
|
1365
|
+
return h || null;
|
|
1366
|
+
} catch {
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
const at = t.lastIndexOf("@");
|
|
1371
|
+
if (at >= 0) t = t.slice(at + 1);
|
|
1372
|
+
t = t.split("/")[0];
|
|
1373
|
+
t = t.replace(/:\d+$/, "");
|
|
1374
|
+
t = t.split(":")[0];
|
|
1375
|
+
t = t.toLowerCase();
|
|
1376
|
+
if (t.length > 253) return null;
|
|
1377
|
+
if (t === "localhost") return t;
|
|
1378
|
+
if (/^[a-z0-9.-]+\.[a-z0-9.-]+$/.test(t)) return t;
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
function destTokensForBinary(binary, args) {
|
|
1382
|
+
const valueFlags = VALUE_FLAGS[binary] ?? /* @__PURE__ */ new Set();
|
|
1383
|
+
const positionals = [];
|
|
1384
|
+
const urlFlagValues = [];
|
|
1385
|
+
for (let i = 0; i < args.length; i++) {
|
|
1386
|
+
const tok = args[i];
|
|
1387
|
+
if (tok === null) continue;
|
|
1388
|
+
if (tok.startsWith("-")) {
|
|
1389
|
+
if (tok.startsWith("--url=")) {
|
|
1390
|
+
urlFlagValues.push(tok.slice("--url=".length));
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
if (tok === "--url") {
|
|
1394
|
+
const next = args[i + 1];
|
|
1395
|
+
if (typeof next === "string") urlFlagValues.push(next);
|
|
1396
|
+
i++;
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (tok.includes("=")) continue;
|
|
1400
|
+
if (valueFlags.has(tok)) i++;
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
positionals.push(tok);
|
|
1404
|
+
}
|
|
1405
|
+
switch (binary) {
|
|
1406
|
+
case "curl":
|
|
1407
|
+
case "wget":
|
|
1408
|
+
return [...urlFlagValues, ...positionals];
|
|
1409
|
+
case "ssh":
|
|
1410
|
+
return positionals.slice(0, 1);
|
|
1411
|
+
case "scp":
|
|
1412
|
+
return positionals.filter((p) => p.includes(":") || p.includes("@"));
|
|
1413
|
+
case "nc":
|
|
1414
|
+
case "ncat":
|
|
1415
|
+
case "netcat":
|
|
1416
|
+
return positionals.slice(0, 1);
|
|
1417
|
+
default:
|
|
1418
|
+
return [];
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function extractShellDestinations(command) {
|
|
1422
|
+
const f = parseShared(command);
|
|
1423
|
+
if (f === PARSE_FAIL) return [];
|
|
1424
|
+
const out = [];
|
|
1425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1426
|
+
try {
|
|
1427
|
+
syntax.Walk(f, (node) => {
|
|
1428
|
+
if (!node) return false;
|
|
1429
|
+
const n = node;
|
|
1430
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1431
|
+
const callArgs = n.Args || [];
|
|
1432
|
+
if (callArgs.length === 0) return true;
|
|
1433
|
+
const name = (resolveWordLiteral(callArgs[0]) || "").toLowerCase();
|
|
1434
|
+
if (!NET_BINARIES.has(name)) return true;
|
|
1435
|
+
const rest = callArgs.slice(1).map((a) => resolveWordLiteral(a));
|
|
1436
|
+
for (const raw of destTokensForBinary(name, rest)) {
|
|
1437
|
+
const host = parseDestHost(raw);
|
|
1438
|
+
if (!host) continue;
|
|
1439
|
+
const key = `${name}:${host}`;
|
|
1440
|
+
if (seen.has(key)) continue;
|
|
1441
|
+
seen.add(key);
|
|
1442
|
+
out.push({ host, binary: name, raw });
|
|
1443
|
+
}
|
|
1444
|
+
return true;
|
|
1445
|
+
});
|
|
1446
|
+
} catch {
|
|
1447
|
+
return out;
|
|
1448
|
+
}
|
|
1449
|
+
return out;
|
|
1450
|
+
}
|
|
1248
1451
|
var FS_OP_CACHE_MAX = 5e3;
|
|
1249
1452
|
var fsOpCache = /* @__PURE__ */ new Map();
|
|
1250
1453
|
function analyzeFsOperation(command) {
|
|
@@ -1374,6 +1577,83 @@ function analyzeShellCommand(command) {
|
|
|
1374
1577
|
}
|
|
1375
1578
|
return { actions, paths, allTokens };
|
|
1376
1579
|
}
|
|
1580
|
+
var DEFAULT_EGRESS_ALLOWLIST = [
|
|
1581
|
+
"*.github.com",
|
|
1582
|
+
"*.githubusercontent.com",
|
|
1583
|
+
"*.npmjs.org",
|
|
1584
|
+
"pypi.org",
|
|
1585
|
+
"*.pythonhosted.org",
|
|
1586
|
+
"crates.io",
|
|
1587
|
+
"*.crates.io",
|
|
1588
|
+
"rubygems.org",
|
|
1589
|
+
"proxy.golang.org",
|
|
1590
|
+
"sum.golang.org",
|
|
1591
|
+
"*.anthropic.com",
|
|
1592
|
+
"*.openai.com",
|
|
1593
|
+
"*.googleapis.com",
|
|
1594
|
+
"*.docker.io",
|
|
1595
|
+
"*.docker.com",
|
|
1596
|
+
"deb.debian.org",
|
|
1597
|
+
"*.ubuntu.com"
|
|
1598
|
+
];
|
|
1599
|
+
function hostMatches(host, pattern) {
|
|
1600
|
+
const h = host.toLowerCase();
|
|
1601
|
+
const p = pattern.toLowerCase().trim();
|
|
1602
|
+
if (!p) return false;
|
|
1603
|
+
if (p === "*") return true;
|
|
1604
|
+
if (p.startsWith("*.")) {
|
|
1605
|
+
const suffix = p.slice(2);
|
|
1606
|
+
return h === suffix || h.endsWith("." + suffix);
|
|
1607
|
+
}
|
|
1608
|
+
return h === p;
|
|
1609
|
+
}
|
|
1610
|
+
function matchesAny(host, patterns) {
|
|
1611
|
+
for (const p of patterns) if (hostMatches(host, p)) return true;
|
|
1612
|
+
return false;
|
|
1613
|
+
}
|
|
1614
|
+
function isPrivateHost(host) {
|
|
1615
|
+
const h = host.toLowerCase();
|
|
1616
|
+
if (h === "localhost" || h === "0.0.0.0") return true;
|
|
1617
|
+
if (h.endsWith(".local") || h.endsWith(".internal") || h.endsWith(".localhost")) return true;
|
|
1618
|
+
if (/^127\./.test(h)) return true;
|
|
1619
|
+
if (/^10\./.test(h)) return true;
|
|
1620
|
+
if (/^192\.168\./.test(h)) return true;
|
|
1621
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
|
|
1622
|
+
return false;
|
|
1623
|
+
}
|
|
1624
|
+
function evaluateEgress(dests, policy) {
|
|
1625
|
+
if (!policy.enabled) return null;
|
|
1626
|
+
let review = null;
|
|
1627
|
+
for (const d of dests) {
|
|
1628
|
+
if (matchesAny(d.host, policy.deny)) {
|
|
1629
|
+
return {
|
|
1630
|
+
verdict: "block",
|
|
1631
|
+
host: d.host,
|
|
1632
|
+
binary: d.binary,
|
|
1633
|
+
reason: `Egress to ${d.host} is on the deny list.`
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
if (policy.allowPrivate && isPrivateHost(d.host)) continue;
|
|
1637
|
+
if (matchesAny(d.host, policy.allow) || matchesAny(d.host, DEFAULT_EGRESS_ALLOWLIST)) continue;
|
|
1638
|
+
if (policy.mode === "block") {
|
|
1639
|
+
return {
|
|
1640
|
+
verdict: "block",
|
|
1641
|
+
host: d.host,
|
|
1642
|
+
binary: d.binary,
|
|
1643
|
+
reason: `Egress to unknown host ${d.host} is blocked (egress policy: block).`
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
if (policy.mode === "review" && !review) {
|
|
1647
|
+
review = {
|
|
1648
|
+
verdict: "review",
|
|
1649
|
+
host: d.host,
|
|
1650
|
+
binary: d.binary,
|
|
1651
|
+
reason: `${d.binary} is sending data to an unrecognized host (${d.host}). Approve this destination?`
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return review;
|
|
1656
|
+
}
|
|
1377
1657
|
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
1378
1658
|
"cat",
|
|
1379
1659
|
"head",
|
|
@@ -1939,6 +2219,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1939
2219
|
}
|
|
1940
2220
|
const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
|
|
1941
2221
|
if (ptVerdict) return ptVerdict;
|
|
2222
|
+
if (config.policy.egress?.enabled) {
|
|
2223
|
+
const dests = extractShellDestinations(shellCommand);
|
|
2224
|
+
if (dests.length > 0) {
|
|
2225
|
+
const eg = evaluateEgress(dests, config.policy.egress);
|
|
2226
|
+
if (eg) {
|
|
2227
|
+
return {
|
|
2228
|
+
decision: eg.verdict,
|
|
2229
|
+
blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
|
|
2230
|
+
reason: eg.reason,
|
|
2231
|
+
ruleName: `egress:${eg.binary}:${eg.host}`,
|
|
2232
|
+
ruleDescription: eg.reason,
|
|
2233
|
+
tier: eg.verdict === "block" ? 3 : 4
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
1942
2238
|
const firstToken = analyzed.actions[0] ?? "";
|
|
1943
2239
|
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
1944
2240
|
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
@@ -2860,6 +3156,32 @@ function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
|
|
|
2860
3156
|
const nextRecords = fresh.slice(-LOOP_MAX_RECORDS);
|
|
2861
3157
|
return { nextRecords, count, looping: count >= threshold };
|
|
2862
3158
|
}
|
|
3159
|
+
var PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
3160
|
+
var PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
3161
|
+
var PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
|
|
3162
|
+
var PII_CC_RE = /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6\d{3})[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/;
|
|
3163
|
+
function detectPii(text) {
|
|
3164
|
+
const found = /* @__PURE__ */ new Set();
|
|
3165
|
+
if (/@/.test(text) && PII_EMAIL_RE.test(text)) found.add("Email");
|
|
3166
|
+
if (/-/.test(text) && PII_SSN_RE.test(text)) found.add("SSN");
|
|
3167
|
+
if (PII_PHONE_RE.test(text)) found.add("Phone");
|
|
3168
|
+
if (PII_CC_RE.test(text)) found.add("Credit Card");
|
|
3169
|
+
return [...found];
|
|
3170
|
+
}
|
|
3171
|
+
var REALTIME_PII_PATTERNS = ["SSN", "Credit Card"];
|
|
3172
|
+
var MAX_PII_SCAN_BYTES = 1e5;
|
|
3173
|
+
function detectArgsPii(args) {
|
|
3174
|
+
if (args === null || args === void 0) return [];
|
|
3175
|
+
let text;
|
|
3176
|
+
try {
|
|
3177
|
+
text = typeof args === "string" ? args : JSON.stringify(args);
|
|
3178
|
+
} catch {
|
|
3179
|
+
return [];
|
|
3180
|
+
}
|
|
3181
|
+
if (typeof text !== "string") return [];
|
|
3182
|
+
if (text.length > MAX_PII_SCAN_BYTES) text = text.slice(0, MAX_PII_SCAN_BYTES);
|
|
3183
|
+
return detectPii(text).filter((p) => REALTIME_PII_PATTERNS.includes(p));
|
|
3184
|
+
}
|
|
2863
3185
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
2864
3186
|
|
|
2865
3187
|
// src/shields.ts
|
|
@@ -3147,7 +3469,8 @@ var DEFAULT_CONFIG = {
|
|
|
3147
3469
|
description: "The AI wants to download a script from the internet and run it immediately, without you seeing what it contains. This is one of the most common ways malware gets installed."
|
|
3148
3470
|
}
|
|
3149
3471
|
],
|
|
3150
|
-
dlp: { enabled: true, scanIgnoredTools: true },
|
|
3472
|
+
dlp: { enabled: true, scanIgnoredTools: true, pii: "off" },
|
|
3473
|
+
egress: { enabled: false, mode: "review", allow: [], deny: [], allowPrivate: true },
|
|
3151
3474
|
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
|
|
3152
3475
|
skillPinning: { enabled: false, mode: "warn", roots: [] }
|
|
3153
3476
|
},
|
|
@@ -3273,6 +3596,11 @@ function getConfig(cwd) {
|
|
|
3273
3596
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
3274
3597
|
},
|
|
3275
3598
|
dlp: { ...DEFAULT_CONFIG.policy.dlp },
|
|
3599
|
+
egress: {
|
|
3600
|
+
...DEFAULT_CONFIG.policy.egress,
|
|
3601
|
+
allow: [...DEFAULT_CONFIG.policy.egress.allow],
|
|
3602
|
+
deny: [...DEFAULT_CONFIG.policy.egress.deny]
|
|
3603
|
+
},
|
|
3276
3604
|
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
|
|
3277
3605
|
skillPinning: {
|
|
3278
3606
|
...DEFAULT_CONFIG.policy.skillPinning,
|
|
@@ -3323,6 +3651,15 @@ function getConfig(cwd) {
|
|
|
3323
3651
|
const d = p.dlp;
|
|
3324
3652
|
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
3325
3653
|
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
3654
|
+
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
3655
|
+
}
|
|
3656
|
+
if (p.egress) {
|
|
3657
|
+
const e = p.egress;
|
|
3658
|
+
if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
|
|
3659
|
+
if (e.mode !== void 0) mergedPolicy.egress.mode = e.mode;
|
|
3660
|
+
if (Array.isArray(e.allow)) mergedPolicy.egress.allow.push(...e.allow);
|
|
3661
|
+
if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
|
|
3662
|
+
if (e.allowPrivate !== void 0) mergedPolicy.egress.allowPrivate = e.allowPrivate;
|
|
3326
3663
|
}
|
|
3327
3664
|
if (p.loopDetection) {
|
|
3328
3665
|
const ld = p.loopDetection;
|
|
@@ -4065,6 +4402,15 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
4065
4402
|
var isTestEnv = () => {
|
|
4066
4403
|
return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
|
|
4067
4404
|
};
|
|
4405
|
+
var MIN_INTERACTION_MS = 400;
|
|
4406
|
+
function resolveNativeDecision(opts) {
|
|
4407
|
+
const { code, output, elapsedMs, locked } = opts;
|
|
4408
|
+
if (locked) return "deny";
|
|
4409
|
+
const tooFast = elapsedMs < MIN_INTERACTION_MS;
|
|
4410
|
+
if (output.includes("Always Allow")) return tooFast ? "deny" : "always_allow";
|
|
4411
|
+
if (code === 0) return tooFast ? "deny" : "allow";
|
|
4412
|
+
return "deny";
|
|
4413
|
+
}
|
|
4068
4414
|
function formatArgs(args, matchedField, matchedWord) {
|
|
4069
4415
|
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
4070
4416
|
let parsed = args;
|
|
@@ -4208,6 +4554,7 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
4208
4554
|
);
|
|
4209
4555
|
return new Promise((resolve) => {
|
|
4210
4556
|
let childProcess = null;
|
|
4557
|
+
const startedAt = Date.now();
|
|
4211
4558
|
const onAbort = () => {
|
|
4212
4559
|
if (childProcess && childProcess.pid) {
|
|
4213
4560
|
try {
|
|
@@ -4265,13 +4612,14 @@ end run`;
|
|
|
4265
4612
|
}
|
|
4266
4613
|
let output = "";
|
|
4267
4614
|
childProcess?.stdout?.on("data", (d) => output += d.toString());
|
|
4268
|
-
childProcess?.on("
|
|
4615
|
+
childProcess?.on("error", () => {
|
|
4269
4616
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
4270
|
-
if (locked) return resolve("deny");
|
|
4271
|
-
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
4272
|
-
if (code === 0) return resolve("allow");
|
|
4273
4617
|
resolve("deny");
|
|
4274
4618
|
});
|
|
4619
|
+
childProcess?.on("close", (code) => {
|
|
4620
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
4621
|
+
resolve(resolveNativeDecision({ code, output, elapsedMs: Date.now() - startedAt, locked }));
|
|
4622
|
+
});
|
|
4275
4623
|
} catch {
|
|
4276
4624
|
resolve("deny");
|
|
4277
4625
|
}
|
|
@@ -4573,6 +4921,37 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4573
4921
|
if (taintResult.tainted && taintResult.record) {
|
|
4574
4922
|
const { path: taintedPath, source: taintSource } = taintResult.record;
|
|
4575
4923
|
taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
|
|
4924
|
+
if (config.policy.egress?.enabled) {
|
|
4925
|
+
const a = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
4926
|
+
const cmd = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : "";
|
|
4927
|
+
const dests = cmd ? extractShellDestinations(cmd) : [];
|
|
4928
|
+
const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
|
|
4929
|
+
if (eg) {
|
|
4930
|
+
if (!isManual)
|
|
4931
|
+
appendLocalAudit(
|
|
4932
|
+
toolName,
|
|
4933
|
+
args,
|
|
4934
|
+
"deny",
|
|
4935
|
+
isObserveMode ? "observe-mode-taint-egress-would-block" : "taint-egress-block",
|
|
4936
|
+
{ ...meta, ruleName: `taint-egress:${eg.host}` },
|
|
4937
|
+
hashAuditArgs
|
|
4938
|
+
);
|
|
4939
|
+
if (isObserveMode) {
|
|
4940
|
+
return {
|
|
4941
|
+
approved: true,
|
|
4942
|
+
checkedBy: "audit",
|
|
4943
|
+
observeWouldBlock: true,
|
|
4944
|
+
blockedByLabel: "\u{1F534} Node9 Taint+Egress (Exfiltration)"
|
|
4945
|
+
};
|
|
4946
|
+
}
|
|
4947
|
+
return {
|
|
4948
|
+
approved: false,
|
|
4949
|
+
reason: `\u{1F534} EXFILTRATION BLOCKED: the tainted file "${taintedPath}" is being sent to untrusted host "${eg.host}". A flagged file leaving to an unrecognized destination is blocked outright.`,
|
|
4950
|
+
blockedBy: "local-config",
|
|
4951
|
+
blockedByLabel: "\u{1F534} Node9 Taint+Egress (Exfiltration Blocked)"
|
|
4952
|
+
};
|
|
4953
|
+
}
|
|
4954
|
+
}
|
|
4576
4955
|
} else if (taintResult.daemonUnavailable) {
|
|
4577
4956
|
taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
|
|
4578
4957
|
}
|
|
@@ -4621,6 +5000,35 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4621
5000
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
4622
5001
|
}
|
|
4623
5002
|
}
|
|
5003
|
+
if (config.policy.dlp.pii === "block" && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
5004
|
+
const piiFound = detectArgsPii(args);
|
|
5005
|
+
if (piiFound.length > 0) {
|
|
5006
|
+
const piiReason = `\u{1F512} PII DETECTED: ${piiFound.join(", ")} found in tool arguments. Remove or tokenize personal data before passing it to a tool.`;
|
|
5007
|
+
if (!isManual)
|
|
5008
|
+
appendLocalAudit(
|
|
5009
|
+
toolName,
|
|
5010
|
+
args,
|
|
5011
|
+
"deny",
|
|
5012
|
+
isObserveMode ? "observe-mode-pii-would-block" : "pii-block",
|
|
5013
|
+
{ ...meta, piiPatterns: piiFound.join(",") },
|
|
5014
|
+
true
|
|
5015
|
+
);
|
|
5016
|
+
if (isObserveMode) {
|
|
5017
|
+
return {
|
|
5018
|
+
approved: true,
|
|
5019
|
+
checkedBy: "audit",
|
|
5020
|
+
observeWouldBlock: true,
|
|
5021
|
+
blockedByLabel: "\u{1F512} Node9 PII (Detected)"
|
|
5022
|
+
};
|
|
5023
|
+
}
|
|
5024
|
+
return {
|
|
5025
|
+
approved: false,
|
|
5026
|
+
reason: piiReason,
|
|
5027
|
+
blockedBy: "local-config",
|
|
5028
|
+
blockedByLabel: "\u{1F512} Node9 PII (Detected)"
|
|
5029
|
+
};
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
4624
5032
|
if (isObserveMode) {
|
|
4625
5033
|
if (!isIgnoredTool2(toolName)) {
|
|
4626
5034
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.31.0",
|
|
4
4
|
"description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|