@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/dashboard.mjs
CHANGED
|
@@ -2241,7 +2241,15 @@ var init_config_schema = __esm({
|
|
|
2241
2241
|
}).optional(),
|
|
2242
2242
|
dlp: z.object({
|
|
2243
2243
|
enabled: z.boolean().optional(),
|
|
2244
|
-
scanIgnoredTools: z.boolean().optional()
|
|
2244
|
+
scanIgnoredTools: z.boolean().optional(),
|
|
2245
|
+
pii: z.enum(["off", "block"]).optional()
|
|
2246
|
+
}).optional(),
|
|
2247
|
+
egress: z.object({
|
|
2248
|
+
enabled: z.boolean().optional(),
|
|
2249
|
+
mode: z.enum(["off", "review", "block"]).optional(),
|
|
2250
|
+
allow: z.array(z.string()).optional(),
|
|
2251
|
+
deny: z.array(z.string()).optional(),
|
|
2252
|
+
allowPrivate: z.boolean().optional()
|
|
2245
2253
|
}).optional(),
|
|
2246
2254
|
loopDetection: z.object({
|
|
2247
2255
|
enabled: z.boolean().optional(),
|
|
@@ -2546,7 +2554,8 @@ var init_config = __esm({
|
|
|
2546
2554
|
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."
|
|
2547
2555
|
}
|
|
2548
2556
|
],
|
|
2549
|
-
dlp: { enabled: true, scanIgnoredTools: true },
|
|
2557
|
+
dlp: { enabled: true, scanIgnoredTools: true, pii: "off" },
|
|
2558
|
+
egress: { enabled: false, mode: "review", allow: [], deny: [], allowPrivate: true },
|
|
2550
2559
|
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
|
|
2551
2560
|
skillPinning: { enabled: false, mode: "warn", roots: [] }
|
|
2552
2561
|
},
|
|
@@ -2642,6 +2651,14 @@ var init_litellm = __esm({
|
|
|
2642
2651
|
}
|
|
2643
2652
|
});
|
|
2644
2653
|
|
|
2654
|
+
// src/cost-codex.ts
|
|
2655
|
+
var init_cost_codex = __esm({
|
|
2656
|
+
"src/cost-codex.ts"() {
|
|
2657
|
+
"use strict";
|
|
2658
|
+
init_litellm();
|
|
2659
|
+
}
|
|
2660
|
+
});
|
|
2661
|
+
|
|
2645
2662
|
// src/costSync.ts
|
|
2646
2663
|
function decodeProjectDirName(dirName) {
|
|
2647
2664
|
return dirName.replace(/-/g, "/");
|
|
@@ -2653,6 +2670,7 @@ var init_costSync = __esm({
|
|
|
2653
2670
|
init_config();
|
|
2654
2671
|
init_audit();
|
|
2655
2672
|
init_litellm();
|
|
2673
|
+
init_cost_codex();
|
|
2656
2674
|
SYNC_INTERVAL_MS = 10 * 60 * 1e3;
|
|
2657
2675
|
}
|
|
2658
2676
|
});
|
package/dist/index.js
CHANGED
|
@@ -293,7 +293,15 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
293
293
|
}).optional(),
|
|
294
294
|
dlp: import_zod.z.object({
|
|
295
295
|
enabled: import_zod.z.boolean().optional(),
|
|
296
|
-
scanIgnoredTools: import_zod.z.boolean().optional()
|
|
296
|
+
scanIgnoredTools: import_zod.z.boolean().optional(),
|
|
297
|
+
pii: import_zod.z.enum(["off", "block"]).optional()
|
|
298
|
+
}).optional(),
|
|
299
|
+
egress: import_zod.z.object({
|
|
300
|
+
enabled: import_zod.z.boolean().optional(),
|
|
301
|
+
mode: import_zod.z.enum(["off", "review", "block"]).optional(),
|
|
302
|
+
allow: import_zod.z.array(import_zod.z.string()).optional(),
|
|
303
|
+
deny: import_zod.z.array(import_zod.z.string()).optional(),
|
|
304
|
+
allowPrivate: import_zod.z.boolean().optional()
|
|
297
305
|
}).optional(),
|
|
298
306
|
loopDetection: import_zod.z.object({
|
|
299
307
|
enabled: import_zod.z.boolean().optional(),
|
|
@@ -1275,6 +1283,201 @@ function extractLiteralArgs(callExpr) {
|
|
|
1275
1283
|
}
|
|
1276
1284
|
return { name, flags, paths };
|
|
1277
1285
|
}
|
|
1286
|
+
var NET_BINARIES = /* @__PURE__ */ new Set(["curl", "wget", "scp", "ssh", "nc", "ncat", "netcat"]);
|
|
1287
|
+
var VALUE_FLAGS = {
|
|
1288
|
+
curl: /* @__PURE__ */ new Set([
|
|
1289
|
+
"-d",
|
|
1290
|
+
"--data",
|
|
1291
|
+
"--data-ascii",
|
|
1292
|
+
"--data-binary",
|
|
1293
|
+
"--data-raw",
|
|
1294
|
+
"--data-urlencode",
|
|
1295
|
+
"-F",
|
|
1296
|
+
"--form",
|
|
1297
|
+
"-H",
|
|
1298
|
+
"--header",
|
|
1299
|
+
"-X",
|
|
1300
|
+
"--request",
|
|
1301
|
+
"-o",
|
|
1302
|
+
"--output",
|
|
1303
|
+
"-T",
|
|
1304
|
+
"--upload-file",
|
|
1305
|
+
"-u",
|
|
1306
|
+
"--user",
|
|
1307
|
+
"-e",
|
|
1308
|
+
"--referer",
|
|
1309
|
+
"-A",
|
|
1310
|
+
"--user-agent",
|
|
1311
|
+
"-b",
|
|
1312
|
+
"--cookie",
|
|
1313
|
+
"-c",
|
|
1314
|
+
"--cookie-jar",
|
|
1315
|
+
"--connect-to",
|
|
1316
|
+
"--resolve",
|
|
1317
|
+
"--cacert",
|
|
1318
|
+
"--cert",
|
|
1319
|
+
"--key",
|
|
1320
|
+
"-x",
|
|
1321
|
+
"--proxy",
|
|
1322
|
+
"-m",
|
|
1323
|
+
"--max-time",
|
|
1324
|
+
"--retry"
|
|
1325
|
+
]),
|
|
1326
|
+
wget: /* @__PURE__ */ new Set([
|
|
1327
|
+
"-O",
|
|
1328
|
+
"--output-document",
|
|
1329
|
+
"--post-data",
|
|
1330
|
+
"--post-file",
|
|
1331
|
+
"--header",
|
|
1332
|
+
"-U",
|
|
1333
|
+
"--user-agent",
|
|
1334
|
+
"--user",
|
|
1335
|
+
"--password",
|
|
1336
|
+
"-o",
|
|
1337
|
+
"--output-file",
|
|
1338
|
+
"-P",
|
|
1339
|
+
"--directory-prefix",
|
|
1340
|
+
"-t",
|
|
1341
|
+
"--tries",
|
|
1342
|
+
"-T",
|
|
1343
|
+
"--timeout"
|
|
1344
|
+
]),
|
|
1345
|
+
scp: /* @__PURE__ */ new Set(["-i", "-F", "-l", "-o", "-c", "-S", "-P", "-J", "-D", "-W"]),
|
|
1346
|
+
ssh: /* @__PURE__ */ new Set([
|
|
1347
|
+
"-i",
|
|
1348
|
+
"-p",
|
|
1349
|
+
"-o",
|
|
1350
|
+
"-l",
|
|
1351
|
+
"-F",
|
|
1352
|
+
"-c",
|
|
1353
|
+
"-L",
|
|
1354
|
+
"-R",
|
|
1355
|
+
"-D",
|
|
1356
|
+
"-W",
|
|
1357
|
+
"-b",
|
|
1358
|
+
"-e",
|
|
1359
|
+
"-m",
|
|
1360
|
+
"-O",
|
|
1361
|
+
"-Q",
|
|
1362
|
+
"-S",
|
|
1363
|
+
"-J",
|
|
1364
|
+
"-w",
|
|
1365
|
+
"-B",
|
|
1366
|
+
"-I",
|
|
1367
|
+
"-E"
|
|
1368
|
+
]),
|
|
1369
|
+
nc: /* @__PURE__ */ new Set(["-p", "-s", "-w", "-X", "-x", "-e", "-g", "-G", "-i", "-O", "-T", "-q", "-m"])
|
|
1370
|
+
};
|
|
1371
|
+
function resolveWordLiteral(w) {
|
|
1372
|
+
const parts = w?.Parts || [];
|
|
1373
|
+
let s = "";
|
|
1374
|
+
for (const p of parts) {
|
|
1375
|
+
const t = syntax.NodeType(p);
|
|
1376
|
+
if (t === "Lit") s += (p.Value ?? "").replace(/\\(.)/g, "$1");
|
|
1377
|
+
else if (t === "SglQuoted") s += p.Value ?? "";
|
|
1378
|
+
else if (t === "DblQuoted") {
|
|
1379
|
+
const inner = p.Parts || [];
|
|
1380
|
+
if (!inner.every((ip) => syntax.NodeType(ip) === "Lit")) return null;
|
|
1381
|
+
s += inner.map((ip) => ip.Value ?? "").join("");
|
|
1382
|
+
} else {
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return s;
|
|
1387
|
+
}
|
|
1388
|
+
function parseDestHost(token) {
|
|
1389
|
+
if (!token) return null;
|
|
1390
|
+
let t = token.trim();
|
|
1391
|
+
if (!t || t.startsWith("-")) return null;
|
|
1392
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(t)) {
|
|
1393
|
+
try {
|
|
1394
|
+
const h = new URL(t).hostname.toLowerCase();
|
|
1395
|
+
return h || null;
|
|
1396
|
+
} catch {
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
const at = t.lastIndexOf("@");
|
|
1401
|
+
if (at >= 0) t = t.slice(at + 1);
|
|
1402
|
+
t = t.split("/")[0];
|
|
1403
|
+
t = t.replace(/:\d+$/, "");
|
|
1404
|
+
t = t.split(":")[0];
|
|
1405
|
+
t = t.toLowerCase();
|
|
1406
|
+
if (t.length > 253) return null;
|
|
1407
|
+
if (t === "localhost") return t;
|
|
1408
|
+
if (/^[a-z0-9.-]+\.[a-z0-9.-]+$/.test(t)) return t;
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
function destTokensForBinary(binary, args) {
|
|
1412
|
+
const valueFlags = VALUE_FLAGS[binary] ?? /* @__PURE__ */ new Set();
|
|
1413
|
+
const positionals = [];
|
|
1414
|
+
const urlFlagValues = [];
|
|
1415
|
+
for (let i = 0; i < args.length; i++) {
|
|
1416
|
+
const tok = args[i];
|
|
1417
|
+
if (tok === null) continue;
|
|
1418
|
+
if (tok.startsWith("-")) {
|
|
1419
|
+
if (tok.startsWith("--url=")) {
|
|
1420
|
+
urlFlagValues.push(tok.slice("--url=".length));
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
if (tok === "--url") {
|
|
1424
|
+
const next = args[i + 1];
|
|
1425
|
+
if (typeof next === "string") urlFlagValues.push(next);
|
|
1426
|
+
i++;
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
if (tok.includes("=")) continue;
|
|
1430
|
+
if (valueFlags.has(tok)) i++;
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
positionals.push(tok);
|
|
1434
|
+
}
|
|
1435
|
+
switch (binary) {
|
|
1436
|
+
case "curl":
|
|
1437
|
+
case "wget":
|
|
1438
|
+
return [...urlFlagValues, ...positionals];
|
|
1439
|
+
case "ssh":
|
|
1440
|
+
return positionals.slice(0, 1);
|
|
1441
|
+
case "scp":
|
|
1442
|
+
return positionals.filter((p) => p.includes(":") || p.includes("@"));
|
|
1443
|
+
case "nc":
|
|
1444
|
+
case "ncat":
|
|
1445
|
+
case "netcat":
|
|
1446
|
+
return positionals.slice(0, 1);
|
|
1447
|
+
default:
|
|
1448
|
+
return [];
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
function extractShellDestinations(command) {
|
|
1452
|
+
const f = parseShared(command);
|
|
1453
|
+
if (f === PARSE_FAIL) return [];
|
|
1454
|
+
const out = [];
|
|
1455
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1456
|
+
try {
|
|
1457
|
+
syntax.Walk(f, (node) => {
|
|
1458
|
+
if (!node) return false;
|
|
1459
|
+
const n = node;
|
|
1460
|
+
if (syntax.NodeType(n) !== "CallExpr") return true;
|
|
1461
|
+
const callArgs = n.Args || [];
|
|
1462
|
+
if (callArgs.length === 0) return true;
|
|
1463
|
+
const name = (resolveWordLiteral(callArgs[0]) || "").toLowerCase();
|
|
1464
|
+
if (!NET_BINARIES.has(name)) return true;
|
|
1465
|
+
const rest = callArgs.slice(1).map((a) => resolveWordLiteral(a));
|
|
1466
|
+
for (const raw of destTokensForBinary(name, rest)) {
|
|
1467
|
+
const host = parseDestHost(raw);
|
|
1468
|
+
if (!host) continue;
|
|
1469
|
+
const key = `${name}:${host}`;
|
|
1470
|
+
if (seen.has(key)) continue;
|
|
1471
|
+
seen.add(key);
|
|
1472
|
+
out.push({ host, binary: name, raw });
|
|
1473
|
+
}
|
|
1474
|
+
return true;
|
|
1475
|
+
});
|
|
1476
|
+
} catch {
|
|
1477
|
+
return out;
|
|
1478
|
+
}
|
|
1479
|
+
return out;
|
|
1480
|
+
}
|
|
1278
1481
|
var FS_OP_CACHE_MAX = 5e3;
|
|
1279
1482
|
var fsOpCache = /* @__PURE__ */ new Map();
|
|
1280
1483
|
function analyzeFsOperation(command) {
|
|
@@ -1404,6 +1607,83 @@ function analyzeShellCommand(command) {
|
|
|
1404
1607
|
}
|
|
1405
1608
|
return { actions, paths, allTokens };
|
|
1406
1609
|
}
|
|
1610
|
+
var DEFAULT_EGRESS_ALLOWLIST = [
|
|
1611
|
+
"*.github.com",
|
|
1612
|
+
"*.githubusercontent.com",
|
|
1613
|
+
"*.npmjs.org",
|
|
1614
|
+
"pypi.org",
|
|
1615
|
+
"*.pythonhosted.org",
|
|
1616
|
+
"crates.io",
|
|
1617
|
+
"*.crates.io",
|
|
1618
|
+
"rubygems.org",
|
|
1619
|
+
"proxy.golang.org",
|
|
1620
|
+
"sum.golang.org",
|
|
1621
|
+
"*.anthropic.com",
|
|
1622
|
+
"*.openai.com",
|
|
1623
|
+
"*.googleapis.com",
|
|
1624
|
+
"*.docker.io",
|
|
1625
|
+
"*.docker.com",
|
|
1626
|
+
"deb.debian.org",
|
|
1627
|
+
"*.ubuntu.com"
|
|
1628
|
+
];
|
|
1629
|
+
function hostMatches(host, pattern) {
|
|
1630
|
+
const h = host.toLowerCase();
|
|
1631
|
+
const p = pattern.toLowerCase().trim();
|
|
1632
|
+
if (!p) return false;
|
|
1633
|
+
if (p === "*") return true;
|
|
1634
|
+
if (p.startsWith("*.")) {
|
|
1635
|
+
const suffix = p.slice(2);
|
|
1636
|
+
return h === suffix || h.endsWith("." + suffix);
|
|
1637
|
+
}
|
|
1638
|
+
return h === p;
|
|
1639
|
+
}
|
|
1640
|
+
function matchesAny(host, patterns) {
|
|
1641
|
+
for (const p of patterns) if (hostMatches(host, p)) return true;
|
|
1642
|
+
return false;
|
|
1643
|
+
}
|
|
1644
|
+
function isPrivateHost(host) {
|
|
1645
|
+
const h = host.toLowerCase();
|
|
1646
|
+
if (h === "localhost" || h === "0.0.0.0") return true;
|
|
1647
|
+
if (h.endsWith(".local") || h.endsWith(".internal") || h.endsWith(".localhost")) return true;
|
|
1648
|
+
if (/^127\./.test(h)) return true;
|
|
1649
|
+
if (/^10\./.test(h)) return true;
|
|
1650
|
+
if (/^192\.168\./.test(h)) return true;
|
|
1651
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
|
|
1652
|
+
return false;
|
|
1653
|
+
}
|
|
1654
|
+
function evaluateEgress(dests, policy) {
|
|
1655
|
+
if (!policy.enabled) return null;
|
|
1656
|
+
let review = null;
|
|
1657
|
+
for (const d of dests) {
|
|
1658
|
+
if (matchesAny(d.host, policy.deny)) {
|
|
1659
|
+
return {
|
|
1660
|
+
verdict: "block",
|
|
1661
|
+
host: d.host,
|
|
1662
|
+
binary: d.binary,
|
|
1663
|
+
reason: `Egress to ${d.host} is on the deny list.`
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
if (policy.allowPrivate && isPrivateHost(d.host)) continue;
|
|
1667
|
+
if (matchesAny(d.host, policy.allow) || matchesAny(d.host, DEFAULT_EGRESS_ALLOWLIST)) continue;
|
|
1668
|
+
if (policy.mode === "block") {
|
|
1669
|
+
return {
|
|
1670
|
+
verdict: "block",
|
|
1671
|
+
host: d.host,
|
|
1672
|
+
binary: d.binary,
|
|
1673
|
+
reason: `Egress to unknown host ${d.host} is blocked (egress policy: block).`
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
if (policy.mode === "review" && !review) {
|
|
1677
|
+
review = {
|
|
1678
|
+
verdict: "review",
|
|
1679
|
+
host: d.host,
|
|
1680
|
+
binary: d.binary,
|
|
1681
|
+
reason: `${d.binary} is sending data to an unrecognized host (${d.host}). Approve this destination?`
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return review;
|
|
1686
|
+
}
|
|
1407
1687
|
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
1408
1688
|
"cat",
|
|
1409
1689
|
"head",
|
|
@@ -1969,6 +2249,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1969
2249
|
}
|
|
1970
2250
|
const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
|
|
1971
2251
|
if (ptVerdict) return ptVerdict;
|
|
2252
|
+
if (config.policy.egress?.enabled) {
|
|
2253
|
+
const dests = extractShellDestinations(shellCommand);
|
|
2254
|
+
if (dests.length > 0) {
|
|
2255
|
+
const eg = evaluateEgress(dests, config.policy.egress);
|
|
2256
|
+
if (eg) {
|
|
2257
|
+
return {
|
|
2258
|
+
decision: eg.verdict,
|
|
2259
|
+
blockedByLabel: eg.verdict === "block" ? "\u{1F310} Node9 Egress (Blocked)" : "\u{1F310} Node9 Egress (Review)",
|
|
2260
|
+
reason: eg.reason,
|
|
2261
|
+
ruleName: `egress:${eg.binary}:${eg.host}`,
|
|
2262
|
+
ruleDescription: eg.reason,
|
|
2263
|
+
tier: eg.verdict === "block" ? 3 : 4
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
1972
2268
|
const firstToken = analyzed.actions[0] ?? "";
|
|
1973
2269
|
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
1974
2270
|
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
@@ -2890,6 +3186,32 @@ function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
|
|
|
2890
3186
|
const nextRecords = fresh.slice(-LOOP_MAX_RECORDS);
|
|
2891
3187
|
return { nextRecords, count, looping: count >= threshold };
|
|
2892
3188
|
}
|
|
3189
|
+
var PII_EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
3190
|
+
var PII_SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
3191
|
+
var PII_PHONE_RE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/;
|
|
3192
|
+
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/;
|
|
3193
|
+
function detectPii(text) {
|
|
3194
|
+
const found = /* @__PURE__ */ new Set();
|
|
3195
|
+
if (/@/.test(text) && PII_EMAIL_RE.test(text)) found.add("Email");
|
|
3196
|
+
if (/-/.test(text) && PII_SSN_RE.test(text)) found.add("SSN");
|
|
3197
|
+
if (PII_PHONE_RE.test(text)) found.add("Phone");
|
|
3198
|
+
if (PII_CC_RE.test(text)) found.add("Credit Card");
|
|
3199
|
+
return [...found];
|
|
3200
|
+
}
|
|
3201
|
+
var REALTIME_PII_PATTERNS = ["SSN", "Credit Card"];
|
|
3202
|
+
var MAX_PII_SCAN_BYTES = 1e5;
|
|
3203
|
+
function detectArgsPii(args) {
|
|
3204
|
+
if (args === null || args === void 0) return [];
|
|
3205
|
+
let text;
|
|
3206
|
+
try {
|
|
3207
|
+
text = typeof args === "string" ? args : JSON.stringify(args);
|
|
3208
|
+
} catch {
|
|
3209
|
+
return [];
|
|
3210
|
+
}
|
|
3211
|
+
if (typeof text !== "string") return [];
|
|
3212
|
+
if (text.length > MAX_PII_SCAN_BYTES) text = text.slice(0, MAX_PII_SCAN_BYTES);
|
|
3213
|
+
return detectPii(text).filter((p) => REALTIME_PII_PATTERNS.includes(p));
|
|
3214
|
+
}
|
|
2893
3215
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
2894
3216
|
|
|
2895
3217
|
// src/shields.ts
|
|
@@ -3177,7 +3499,8 @@ var DEFAULT_CONFIG = {
|
|
|
3177
3499
|
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."
|
|
3178
3500
|
}
|
|
3179
3501
|
],
|
|
3180
|
-
dlp: { enabled: true, scanIgnoredTools: true },
|
|
3502
|
+
dlp: { enabled: true, scanIgnoredTools: true, pii: "off" },
|
|
3503
|
+
egress: { enabled: false, mode: "review", allow: [], deny: [], allowPrivate: true },
|
|
3181
3504
|
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
|
|
3182
3505
|
skillPinning: { enabled: false, mode: "warn", roots: [] }
|
|
3183
3506
|
},
|
|
@@ -3303,6 +3626,11 @@ function getConfig(cwd) {
|
|
|
3303
3626
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
3304
3627
|
},
|
|
3305
3628
|
dlp: { ...DEFAULT_CONFIG.policy.dlp },
|
|
3629
|
+
egress: {
|
|
3630
|
+
...DEFAULT_CONFIG.policy.egress,
|
|
3631
|
+
allow: [...DEFAULT_CONFIG.policy.egress.allow],
|
|
3632
|
+
deny: [...DEFAULT_CONFIG.policy.egress.deny]
|
|
3633
|
+
},
|
|
3306
3634
|
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
|
|
3307
3635
|
skillPinning: {
|
|
3308
3636
|
...DEFAULT_CONFIG.policy.skillPinning,
|
|
@@ -3353,6 +3681,15 @@ function getConfig(cwd) {
|
|
|
3353
3681
|
const d = p.dlp;
|
|
3354
3682
|
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
3355
3683
|
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
3684
|
+
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
3685
|
+
}
|
|
3686
|
+
if (p.egress) {
|
|
3687
|
+
const e = p.egress;
|
|
3688
|
+
if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
|
|
3689
|
+
if (e.mode !== void 0) mergedPolicy.egress.mode = e.mode;
|
|
3690
|
+
if (Array.isArray(e.allow)) mergedPolicy.egress.allow.push(...e.allow);
|
|
3691
|
+
if (Array.isArray(e.deny)) mergedPolicy.egress.deny.push(...e.deny);
|
|
3692
|
+
if (e.allowPrivate !== void 0) mergedPolicy.egress.allowPrivate = e.allowPrivate;
|
|
3356
3693
|
}
|
|
3357
3694
|
if (p.loopDetection) {
|
|
3358
3695
|
const ld = p.loopDetection;
|
|
@@ -4095,6 +4432,15 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
4095
4432
|
var isTestEnv = () => {
|
|
4096
4433
|
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";
|
|
4097
4434
|
};
|
|
4435
|
+
var MIN_INTERACTION_MS = 400;
|
|
4436
|
+
function resolveNativeDecision(opts) {
|
|
4437
|
+
const { code, output, elapsedMs, locked } = opts;
|
|
4438
|
+
if (locked) return "deny";
|
|
4439
|
+
const tooFast = elapsedMs < MIN_INTERACTION_MS;
|
|
4440
|
+
if (output.includes("Always Allow")) return tooFast ? "deny" : "always_allow";
|
|
4441
|
+
if (code === 0) return tooFast ? "deny" : "allow";
|
|
4442
|
+
return "deny";
|
|
4443
|
+
}
|
|
4098
4444
|
function formatArgs(args, matchedField, matchedWord) {
|
|
4099
4445
|
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
4100
4446
|
let parsed = args;
|
|
@@ -4238,6 +4584,7 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
4238
4584
|
);
|
|
4239
4585
|
return new Promise((resolve) => {
|
|
4240
4586
|
let childProcess = null;
|
|
4587
|
+
const startedAt = Date.now();
|
|
4241
4588
|
const onAbort = () => {
|
|
4242
4589
|
if (childProcess && childProcess.pid) {
|
|
4243
4590
|
try {
|
|
@@ -4295,13 +4642,14 @@ end run`;
|
|
|
4295
4642
|
}
|
|
4296
4643
|
let output = "";
|
|
4297
4644
|
childProcess?.stdout?.on("data", (d) => output += d.toString());
|
|
4298
|
-
childProcess?.on("
|
|
4645
|
+
childProcess?.on("error", () => {
|
|
4299
4646
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
4300
|
-
if (locked) return resolve("deny");
|
|
4301
|
-
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
4302
|
-
if (code === 0) return resolve("allow");
|
|
4303
4647
|
resolve("deny");
|
|
4304
4648
|
});
|
|
4649
|
+
childProcess?.on("close", (code) => {
|
|
4650
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
4651
|
+
resolve(resolveNativeDecision({ code, output, elapsedMs: Date.now() - startedAt, locked }));
|
|
4652
|
+
});
|
|
4305
4653
|
} catch {
|
|
4306
4654
|
resolve("deny");
|
|
4307
4655
|
}
|
|
@@ -4603,6 +4951,37 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4603
4951
|
if (taintResult.tainted && taintResult.record) {
|
|
4604
4952
|
const { path: taintedPath, source: taintSource } = taintResult.record;
|
|
4605
4953
|
taintWarning = `\u26A0\uFE0F ${taintedPath} was flagged by ${taintSource} \u2014 this file may contain sensitive data`;
|
|
4954
|
+
if (config.policy.egress?.enabled) {
|
|
4955
|
+
const a = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
4956
|
+
const cmd = typeof a.command === "string" ? a.command : typeof a.cmd === "string" ? a.cmd : "";
|
|
4957
|
+
const dests = cmd ? extractShellDestinations(cmd) : [];
|
|
4958
|
+
const eg = dests.length > 0 ? evaluateEgress(dests, config.policy.egress) : null;
|
|
4959
|
+
if (eg) {
|
|
4960
|
+
if (!isManual)
|
|
4961
|
+
appendLocalAudit(
|
|
4962
|
+
toolName,
|
|
4963
|
+
args,
|
|
4964
|
+
"deny",
|
|
4965
|
+
isObserveMode ? "observe-mode-taint-egress-would-block" : "taint-egress-block",
|
|
4966
|
+
{ ...meta, ruleName: `taint-egress:${eg.host}` },
|
|
4967
|
+
hashAuditArgs
|
|
4968
|
+
);
|
|
4969
|
+
if (isObserveMode) {
|
|
4970
|
+
return {
|
|
4971
|
+
approved: true,
|
|
4972
|
+
checkedBy: "audit",
|
|
4973
|
+
observeWouldBlock: true,
|
|
4974
|
+
blockedByLabel: "\u{1F534} Node9 Taint+Egress (Exfiltration)"
|
|
4975
|
+
};
|
|
4976
|
+
}
|
|
4977
|
+
return {
|
|
4978
|
+
approved: false,
|
|
4979
|
+
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.`,
|
|
4980
|
+
blockedBy: "local-config",
|
|
4981
|
+
blockedByLabel: "\u{1F534} Node9 Taint+Egress (Exfiltration Blocked)"
|
|
4982
|
+
};
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4606
4985
|
} else if (taintResult.daemonUnavailable) {
|
|
4607
4986
|
taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
|
|
4608
4987
|
}
|
|
@@ -4651,6 +5030,35 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4651
5030
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
4652
5031
|
}
|
|
4653
5032
|
}
|
|
5033
|
+
if (config.policy.dlp.pii === "block" && (!isIgnoredTool2(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
5034
|
+
const piiFound = detectArgsPii(args);
|
|
5035
|
+
if (piiFound.length > 0) {
|
|
5036
|
+
const piiReason = `\u{1F512} PII DETECTED: ${piiFound.join(", ")} found in tool arguments. Remove or tokenize personal data before passing it to a tool.`;
|
|
5037
|
+
if (!isManual)
|
|
5038
|
+
appendLocalAudit(
|
|
5039
|
+
toolName,
|
|
5040
|
+
args,
|
|
5041
|
+
"deny",
|
|
5042
|
+
isObserveMode ? "observe-mode-pii-would-block" : "pii-block",
|
|
5043
|
+
{ ...meta, piiPatterns: piiFound.join(",") },
|
|
5044
|
+
true
|
|
5045
|
+
);
|
|
5046
|
+
if (isObserveMode) {
|
|
5047
|
+
return {
|
|
5048
|
+
approved: true,
|
|
5049
|
+
checkedBy: "audit",
|
|
5050
|
+
observeWouldBlock: true,
|
|
5051
|
+
blockedByLabel: "\u{1F512} Node9 PII (Detected)"
|
|
5052
|
+
};
|
|
5053
|
+
}
|
|
5054
|
+
return {
|
|
5055
|
+
approved: false,
|
|
5056
|
+
reason: piiReason,
|
|
5057
|
+
blockedBy: "local-config",
|
|
5058
|
+
blockedByLabel: "\u{1F512} Node9 PII (Detected)"
|
|
5059
|
+
};
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
4654
5062
|
if (isObserveMode) {
|
|
4655
5063
|
if (!isIgnoredTool2(toolName)) {
|
|
4656
5064
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|