@node9/proxy 1.29.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 +1653 -908
- package/dist/cli.mjs +1622 -878
- package/dist/dashboard.mjs +29 -4
- package/dist/index.js +478 -155
- package/dist/index.mjs +475 -152
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -74,8 +74,13 @@ __export(audit_exports, {
|
|
|
74
74
|
appendHookDebug: () => appendHookDebug,
|
|
75
75
|
appendLocalAudit: () => appendLocalAudit,
|
|
76
76
|
appendToLog: () => appendToLog,
|
|
77
|
+
buildArgsPreview: () => buildArgsPreview,
|
|
78
|
+
generateEventId: () => generateEventId,
|
|
77
79
|
redactSecrets: () => redactSecrets
|
|
78
80
|
});
|
|
81
|
+
function generateEventId() {
|
|
82
|
+
return `${Date.now().toString(36)}-${import_crypto2.default.randomBytes(6).toString("hex")}`;
|
|
83
|
+
}
|
|
79
84
|
function isTestCall(toolName, args) {
|
|
80
85
|
if (toolName !== "Bash" && toolName !== "bash") return false;
|
|
81
86
|
const cmd = args?.command;
|
|
@@ -94,6 +99,17 @@ function redactSecrets(text) {
|
|
|
94
99
|
);
|
|
95
100
|
return redacted;
|
|
96
101
|
}
|
|
102
|
+
function buildArgsPreview(args) {
|
|
103
|
+
try {
|
|
104
|
+
const o = args && typeof args === "object" ? args : null;
|
|
105
|
+
const primary = o && (o.command ?? o.file_path ?? o.path ?? o.url ?? o.query);
|
|
106
|
+
const text = typeof primary === "string" ? primary : args ? JSON.stringify(args) : "";
|
|
107
|
+
if (!text) return void 0;
|
|
108
|
+
return redactSecrets(text).slice(0, 120);
|
|
109
|
+
} catch {
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
97
113
|
function appendToLog(logPath, entry) {
|
|
98
114
|
try {
|
|
99
115
|
const dir = import_path.default.dirname(logPath);
|
|
@@ -116,11 +132,18 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
|
116
132
|
});
|
|
117
133
|
}
|
|
118
134
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
119
|
-
const
|
|
135
|
+
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
|
|
136
|
+
const preview = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
|
|
137
|
+
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args), ...preview ? { argsPreview: preview } : {} } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
120
138
|
const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
|
|
121
139
|
const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
|
|
122
140
|
const agentToolNameField = meta?.agentToolName ? { agentToolName: meta.agentToolName } : {};
|
|
141
|
+
const dlpFields = meta?.dlpPattern ? { dlpPattern: meta.dlpPattern, dlpSample: meta.dlpSample } : {};
|
|
142
|
+
const cloudLinkField = meta?.cloudRequestId ? { cloudRequestId: meta.cloudRequestId } : {};
|
|
123
143
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
144
|
+
// eid first: the outbox shipper dedups on it, and a fixed leading field
|
|
145
|
+
// makes the JSONL easy to eyeball.
|
|
146
|
+
eid: generateEventId(),
|
|
124
147
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
125
148
|
tool: toolName,
|
|
126
149
|
...agentToolNameField,
|
|
@@ -128,6 +151,8 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
|
|
|
128
151
|
decision,
|
|
129
152
|
checkedBy,
|
|
130
153
|
...ruleNameField,
|
|
154
|
+
...dlpFields,
|
|
155
|
+
...cloudLinkField,
|
|
131
156
|
...testRun,
|
|
132
157
|
agent: meta?.agent,
|
|
133
158
|
mcpServer: meta?.mcpServer,
|
|
@@ -142,13 +167,14 @@ function appendConfigAudit(entry) {
|
|
|
142
167
|
hostname: import_os.default.hostname()
|
|
143
168
|
});
|
|
144
169
|
}
|
|
145
|
-
var import_fs, import_path, import_os, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, TEST_COMMAND_RE;
|
|
170
|
+
var import_fs, import_path, import_os, import_crypto2, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, TEST_COMMAND_RE;
|
|
146
171
|
var init_audit = __esm({
|
|
147
172
|
"src/audit/index.ts"() {
|
|
148
173
|
"use strict";
|
|
149
174
|
import_fs = __toESM(require("fs"));
|
|
150
175
|
import_path = __toESM(require("path"));
|
|
151
176
|
import_os = __toESM(require("os"));
|
|
177
|
+
import_crypto2 = __toESM(require("crypto"));
|
|
152
178
|
init_hasher();
|
|
153
179
|
LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
154
180
|
HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
|
|
@@ -246,7 +272,13 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
246
272
|
allowGlobalPause: import_zod.z.boolean().optional(),
|
|
247
273
|
auditHashArgs: import_zod.z.boolean().optional(),
|
|
248
274
|
agentPolicy: import_zod.z.enum(["require_approval", "block_on_rules"]).optional(),
|
|
249
|
-
cloudSyncIntervalHours: import_zod.z.number().positive().optional()
|
|
275
|
+
cloudSyncIntervalHours: import_zod.z.number().positive().optional(),
|
|
276
|
+
// Outbox shipper (audit.log → SaaS batch ingest). enabled defaults
|
|
277
|
+
// to true; set false to fall back to local-only auditing.
|
|
278
|
+
shipper: import_zod.z.object({
|
|
279
|
+
enabled: import_zod.z.boolean().optional(),
|
|
280
|
+
intervalSeconds: import_zod.z.number().min(5).optional()
|
|
281
|
+
}).optional()
|
|
250
282
|
}).optional(),
|
|
251
283
|
policy: import_zod.z.object({
|
|
252
284
|
sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
@@ -261,7 +293,15 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
261
293
|
}).optional(),
|
|
262
294
|
dlp: import_zod.z.object({
|
|
263
295
|
enabled: import_zod.z.boolean().optional(),
|
|
264
|
-
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()
|
|
265
305
|
}).optional(),
|
|
266
306
|
loopDetection: import_zod.z.object({
|
|
267
307
|
enabled: import_zod.z.boolean().optional(),
|
|
@@ -314,7 +354,7 @@ var import_mvdan_sh = __toESM(require("mvdan-sh"), 1);
|
|
|
314
354
|
var import_picomatch = __toESM(require("picomatch"), 1);
|
|
315
355
|
var import_safe_regex22 = __toESM(require("safe-regex2"), 1);
|
|
316
356
|
var import_safe_regex23 = __toESM(require("safe-regex2"), 1);
|
|
317
|
-
var
|
|
357
|
+
var import_crypto3 = __toESM(require("crypto"), 1);
|
|
318
358
|
var ASSIGNMENT_CONTEXT_RE = /\b(?:password|passwd|secret|token|api[_-]?key|auth(?:_key|_token)?|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*/i;
|
|
319
359
|
function isAssignmentContext(text) {
|
|
320
360
|
return ASSIGNMENT_CONTEXT_RE.test(text);
|
|
@@ -1243,6 +1283,201 @@ function extractLiteralArgs(callExpr) {
|
|
|
1243
1283
|
}
|
|
1244
1284
|
return { name, flags, paths };
|
|
1245
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
|
+
}
|
|
1246
1481
|
var FS_OP_CACHE_MAX = 5e3;
|
|
1247
1482
|
var fsOpCache = /* @__PURE__ */ new Map();
|
|
1248
1483
|
function analyzeFsOperation(command) {
|
|
@@ -1372,6 +1607,83 @@ function analyzeShellCommand(command) {
|
|
|
1372
1607
|
}
|
|
1373
1608
|
return { actions, paths, allTokens };
|
|
1374
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
|
+
}
|
|
1375
1687
|
var SOURCE_COMMANDS = /* @__PURE__ */ new Set([
|
|
1376
1688
|
"cat",
|
|
1377
1689
|
"head",
|
|
@@ -1937,6 +2249,22 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1937
2249
|
}
|
|
1938
2250
|
const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
|
|
1939
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
|
+
}
|
|
1940
2268
|
const firstToken = analyzed.actions[0] ?? "";
|
|
1941
2269
|
if (["ssh", "scp", "rsync"].includes(firstToken)) {
|
|
1942
2270
|
const rawTokens = shellCommand.trim().split(/\s+/);
|
|
@@ -2847,7 +3175,7 @@ assertBuiltinShieldRegexesAreSafe();
|
|
|
2847
3175
|
var LOOP_MAX_RECORDS = 500;
|
|
2848
3176
|
function computeArgsHash(args) {
|
|
2849
3177
|
const str = JSON.stringify(args ?? "");
|
|
2850
|
-
return
|
|
3178
|
+
return import_crypto3.default.createHash("sha256").update(str).digest("hex").slice(0, 16);
|
|
2851
3179
|
}
|
|
2852
3180
|
function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
|
|
2853
3181
|
const hash = computeArgsHash(args);
|
|
@@ -2858,6 +3186,32 @@ function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
|
|
|
2858
3186
|
const nextRecords = fresh.slice(-LOOP_MAX_RECORDS);
|
|
2859
3187
|
return { nextRecords, count, looping: count >= threshold };
|
|
2860
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
|
+
}
|
|
2861
3215
|
var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
2862
3216
|
|
|
2863
3217
|
// src/shields.ts
|
|
@@ -2960,7 +3314,8 @@ var DEFAULT_CONFIG = {
|
|
|
2960
3314
|
flightRecorder: true,
|
|
2961
3315
|
auditHashArgs: true,
|
|
2962
3316
|
approvers: { native: true, browser: false, cloud: false, terminal: true },
|
|
2963
|
-
cloudSyncIntervalHours: 5
|
|
3317
|
+
cloudSyncIntervalHours: 5,
|
|
3318
|
+
shipper: { enabled: true, intervalSeconds: 20 }
|
|
2964
3319
|
},
|
|
2965
3320
|
policy: {
|
|
2966
3321
|
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
@@ -3144,7 +3499,8 @@ var DEFAULT_CONFIG = {
|
|
|
3144
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."
|
|
3145
3500
|
}
|
|
3146
3501
|
],
|
|
3147
|
-
dlp: { enabled: true, scanIgnoredTools: true },
|
|
3502
|
+
dlp: { enabled: true, scanIgnoredTools: true, pii: "off" },
|
|
3503
|
+
egress: { enabled: false, mode: "review", allow: [], deny: [], allowPrivate: true },
|
|
3148
3504
|
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 },
|
|
3149
3505
|
skillPinning: { enabled: false, mode: "warn", roots: [] }
|
|
3150
3506
|
},
|
|
@@ -3255,7 +3611,8 @@ function getConfig(cwd) {
|
|
|
3255
3611
|
const projectConfig = tryLoadConfig(projectPath);
|
|
3256
3612
|
const mergedSettings = {
|
|
3257
3613
|
...DEFAULT_CONFIG.settings,
|
|
3258
|
-
approvers: { ...DEFAULT_CONFIG.settings.approvers }
|
|
3614
|
+
approvers: { ...DEFAULT_CONFIG.settings.approvers },
|
|
3615
|
+
shipper: { ...DEFAULT_CONFIG.settings.shipper }
|
|
3259
3616
|
};
|
|
3260
3617
|
const mergedPolicy = {
|
|
3261
3618
|
sandboxPaths: [...DEFAULT_CONFIG.policy.sandboxPaths],
|
|
@@ -3269,6 +3626,11 @@ function getConfig(cwd) {
|
|
|
3269
3626
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
3270
3627
|
},
|
|
3271
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
|
+
},
|
|
3272
3634
|
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection },
|
|
3273
3635
|
skillPinning: {
|
|
3274
3636
|
...DEFAULT_CONFIG.policy.skillPinning,
|
|
@@ -3286,6 +3648,7 @@ function getConfig(cwd) {
|
|
|
3286
3648
|
if (s.enableHookLogDebug !== void 0)
|
|
3287
3649
|
mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
|
|
3288
3650
|
if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
|
|
3651
|
+
if (s.shipper) mergedSettings.shipper = { ...mergedSettings.shipper, ...s.shipper };
|
|
3289
3652
|
if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
|
|
3290
3653
|
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
3291
3654
|
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
@@ -3318,6 +3681,15 @@ function getConfig(cwd) {
|
|
|
3318
3681
|
const d = p.dlp;
|
|
3319
3682
|
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
3320
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;
|
|
3321
3693
|
}
|
|
3322
3694
|
if (p.loopDetection) {
|
|
3323
3695
|
const ld = p.loopDetection;
|
|
@@ -3948,7 +4320,7 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
3948
4320
|
}
|
|
3949
4321
|
|
|
3950
4322
|
// src/auth/orchestrator.ts
|
|
3951
|
-
var
|
|
4323
|
+
var import_crypto4 = require("crypto");
|
|
3952
4324
|
|
|
3953
4325
|
// src/ui/native.ts
|
|
3954
4326
|
var import_child_process = require("child_process");
|
|
@@ -4060,6 +4432,15 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
4060
4432
|
var isTestEnv = () => {
|
|
4061
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";
|
|
4062
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
|
+
}
|
|
4063
4444
|
function formatArgs(args, matchedField, matchedWord) {
|
|
4064
4445
|
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
4065
4446
|
let parsed = args;
|
|
@@ -4203,6 +4584,7 @@ async function askNativePopup(toolName, args, agent, explainableLabel, locked =
|
|
|
4203
4584
|
);
|
|
4204
4585
|
return new Promise((resolve) => {
|
|
4205
4586
|
let childProcess = null;
|
|
4587
|
+
const startedAt = Date.now();
|
|
4206
4588
|
const onAbort = () => {
|
|
4207
4589
|
if (childProcess && childProcess.pid) {
|
|
4208
4590
|
try {
|
|
@@ -4260,13 +4642,14 @@ end run`;
|
|
|
4260
4642
|
}
|
|
4261
4643
|
let output = "";
|
|
4262
4644
|
childProcess?.stdout?.on("data", (d) => output += d.toString());
|
|
4263
|
-
childProcess?.on("
|
|
4645
|
+
childProcess?.on("error", () => {
|
|
4264
4646
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
4265
|
-
if (locked) return resolve("deny");
|
|
4266
|
-
if (output.includes("Always Allow")) return resolve("always_allow");
|
|
4267
|
-
if (code === 0) return resolve("allow");
|
|
4268
4647
|
resolve("deny");
|
|
4269
4648
|
});
|
|
4649
|
+
childProcess?.on("close", (code) => {
|
|
4650
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
4651
|
+
resolve(resolveNativeDecision({ code, output, elapsedMs: Date.now() - startedAt, locked }));
|
|
4652
|
+
});
|
|
4270
4653
|
} catch {
|
|
4271
4654
|
resolve("deny");
|
|
4272
4655
|
}
|
|
@@ -4281,91 +4664,6 @@ var import_fs9 = __toESM(require("fs"));
|
|
|
4281
4664
|
var import_os8 = __toESM(require("os"));
|
|
4282
4665
|
var import_path11 = __toESM(require("path"));
|
|
4283
4666
|
init_audit();
|
|
4284
|
-
var DLP_SAMPLE_MAX_LEN = 200;
|
|
4285
|
-
var DLP_PATTERN_MAX_LEN = 100;
|
|
4286
|
-
var KNOWN_CHECKED_BY = /* @__PURE__ */ new Set([
|
|
4287
|
-
"dlp-block",
|
|
4288
|
-
"observe-mode-dlp-would-block",
|
|
4289
|
-
"dlp-review-flagged",
|
|
4290
|
-
"loop-detected",
|
|
4291
|
-
"audit-mode",
|
|
4292
|
-
"local-policy",
|
|
4293
|
-
"smart-rule-block",
|
|
4294
|
-
// Smart-rule block was downgraded to review because the daemon was
|
|
4295
|
-
// running and we're not in CI. The block attempt is still recorded;
|
|
4296
|
-
// the user got a popup. Distinct from 'smart-rule-block' so the
|
|
4297
|
-
// dashboard can show "block rule overridden" separately from a hard
|
|
4298
|
-
// block that fired with no human in the loop.
|
|
4299
|
-
"smart-rule-block-override",
|
|
4300
|
-
"persistent",
|
|
4301
|
-
"trust",
|
|
4302
|
-
"observe-mode",
|
|
4303
|
-
"observe-mode-would-block"
|
|
4304
|
-
]);
|
|
4305
|
-
function validateApiUrl(raw) {
|
|
4306
|
-
let u;
|
|
4307
|
-
try {
|
|
4308
|
-
u = new URL(raw);
|
|
4309
|
-
} catch {
|
|
4310
|
-
return null;
|
|
4311
|
-
}
|
|
4312
|
-
if (u.username || u.password) return null;
|
|
4313
|
-
if (u.protocol === "https:") return u;
|
|
4314
|
-
if (u.protocol === "http:") {
|
|
4315
|
-
const h = u.hostname;
|
|
4316
|
-
if (h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]") return u;
|
|
4317
|
-
}
|
|
4318
|
-
return null;
|
|
4319
|
-
}
|
|
4320
|
-
function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false, riskMetadata) {
|
|
4321
|
-
const validated = validateApiUrl(creds.apiUrl);
|
|
4322
|
-
if (!validated) {
|
|
4323
|
-
try {
|
|
4324
|
-
import_fs9.default.appendFileSync(
|
|
4325
|
-
HOOK_DEBUG_LOG,
|
|
4326
|
-
`[audit] refused to send: invalid apiUrl scheme/host (got "${String(creds.apiUrl).slice(0, 200)}")
|
|
4327
|
-
`
|
|
4328
|
-
);
|
|
4329
|
-
} catch {
|
|
4330
|
-
}
|
|
4331
|
-
return Promise.resolve();
|
|
4332
|
-
}
|
|
4333
|
-
const safeArgs = containsSensitiveArgs ? { tool: toolName, redacted: true } : args;
|
|
4334
|
-
const dlpSample = dlpInfo && typeof dlpInfo.redactedSample === "string" ? dlpInfo.redactedSample.slice(0, DLP_SAMPLE_MAX_LEN) : void 0;
|
|
4335
|
-
const dlpPattern = dlpInfo && typeof dlpInfo.pattern === "string" ? dlpInfo.pattern.slice(0, DLP_PATTERN_MAX_LEN) : void 0;
|
|
4336
|
-
const safeCheckedBy = KNOWN_CHECKED_BY.has(checkedBy) ? checkedBy : "unknown";
|
|
4337
|
-
const cleanedRiskMetadata = riskMetadata ? Object.fromEntries(
|
|
4338
|
-
Object.entries(riskMetadata).filter(([, v]) => typeof v === "string" && v.length > 0)
|
|
4339
|
-
) : void 0;
|
|
4340
|
-
const hasRiskMetadata = cleanedRiskMetadata && Object.keys(cleanedRiskMetadata).length > 0;
|
|
4341
|
-
return fetch(`${validated.toString().replace(/\/$/, "")}/audit`, {
|
|
4342
|
-
method: "POST",
|
|
4343
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
4344
|
-
body: JSON.stringify({
|
|
4345
|
-
toolName,
|
|
4346
|
-
args: safeArgs,
|
|
4347
|
-
checkedBy: safeCheckedBy,
|
|
4348
|
-
...dlpInfo && { dlpPattern, dlpSample },
|
|
4349
|
-
...hasRiskMetadata && { riskMetadata: cleanedRiskMetadata },
|
|
4350
|
-
// session_id (Claude Code + Gemini CLI) groups all audit rows from one
|
|
4351
|
-
// agent run; transcript_path is the authoritative pointer to the
|
|
4352
|
-
// session log (survives Gemini resume drift). Both optional —
|
|
4353
|
-
// unsupported agents (MCP-mediated) leave them undefined.
|
|
4354
|
-
...meta?.sessionId && { runId: meta.sessionId },
|
|
4355
|
-
...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
|
|
4356
|
-
context: {
|
|
4357
|
-
agent: meta?.agent,
|
|
4358
|
-
mcpServer: meta?.mcpServer,
|
|
4359
|
-
hostname: import_os8.default.hostname(),
|
|
4360
|
-
cwd: process.cwd(),
|
|
4361
|
-
platform: import_os8.default.platform()
|
|
4362
|
-
}
|
|
4363
|
-
}),
|
|
4364
|
-
signal: AbortSignal.timeout(5e3)
|
|
4365
|
-
}).then(() => {
|
|
4366
|
-
}).catch(() => {
|
|
4367
|
-
});
|
|
4368
|
-
}
|
|
4369
4667
|
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
|
|
4370
4668
|
const controller = new AbortController();
|
|
4371
4669
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
@@ -4576,7 +4874,7 @@ function notifyActivity(data) {
|
|
|
4576
4874
|
}
|
|
4577
4875
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
4578
4876
|
if (!options?.calledFromDaemon) {
|
|
4579
|
-
const actId = (0,
|
|
4877
|
+
const actId = (0, import_crypto4.randomUUID)();
|
|
4580
4878
|
const actTs = Date.now();
|
|
4581
4879
|
const stripAnsi = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
|
|
4582
4880
|
const sanitizedAgent = meta?.agent ? stripAnsi(meta.agent).slice(0, 80) : void 0;
|
|
@@ -4653,6 +4951,37 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4653
4951
|
if (taintResult.tainted && taintResult.record) {
|
|
4654
4952
|
const { path: taintedPath, source: taintSource } = taintResult.record;
|
|
4655
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
|
+
}
|
|
4656
4985
|
} else if (taintResult.daemonUnavailable) {
|
|
4657
4986
|
taintWarning = `\u26A0\uFE0F Taint service unavailable \u2014 cannot verify if ${filePaths.join(", ")} is clean`;
|
|
4658
4987
|
}
|
|
@@ -4671,17 +5000,11 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4671
5000
|
args,
|
|
4672
5001
|
"deny",
|
|
4673
5002
|
isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
toolName,
|
|
4680
|
-
args,
|
|
4681
|
-
isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
|
|
4682
|
-
creds,
|
|
4683
|
-
meta,
|
|
4684
|
-
{ pattern: dlpMatch.patternName, redactedSample: dlpMatch.redactedSample },
|
|
5003
|
+
{
|
|
5004
|
+
...meta,
|
|
5005
|
+
dlpPattern: dlpMatch.patternName,
|
|
5006
|
+
dlpSample: dlpMatch.redactedSample
|
|
5007
|
+
},
|
|
4685
5008
|
true
|
|
4686
5009
|
);
|
|
4687
5010
|
if (isWriteTool(toolName) && filePath) {
|
|
@@ -4707,6 +5030,35 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4707
5030
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
4708
5031
|
}
|
|
4709
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
|
+
}
|
|
4710
5062
|
if (isObserveMode) {
|
|
4711
5063
|
if (!isIgnoredTool2(toolName)) {
|
|
4712
5064
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
@@ -4737,9 +5089,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4737
5089
|
const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
|
|
4738
5090
|
if (policyResult.decision === "review") {
|
|
4739
5091
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
|
|
4740
|
-
if (approvers.cloud && creds?.apiKey) {
|
|
4741
|
-
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
4742
|
-
}
|
|
4743
5092
|
}
|
|
4744
5093
|
}
|
|
4745
5094
|
return { approved: true, checkedBy: "audit" };
|
|
@@ -4752,8 +5101,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4752
5101
|
const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
|
|
4753
5102
|
if (!isManual)
|
|
4754
5103
|
appendLocalAudit(toolName, args, "deny", "loop-detected", meta, hashAuditArgs);
|
|
4755
|
-
if (approvers.cloud && creds?.apiKey)
|
|
4756
|
-
auditLocalAllow(toolName, args, "loop-detected", creds, meta, void 0, true);
|
|
4757
5104
|
return {
|
|
4758
5105
|
approved: false,
|
|
4759
5106
|
reason,
|
|
@@ -4772,15 +5119,15 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4772
5119
|
};
|
|
4773
5120
|
}
|
|
4774
5121
|
if (policyResult.decision === "allow") {
|
|
4775
|
-
if (
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
5122
|
+
if (!isManual)
|
|
5123
|
+
appendLocalAudit(
|
|
5124
|
+
toolName,
|
|
5125
|
+
args,
|
|
5126
|
+
"allow",
|
|
5127
|
+
"local-policy",
|
|
5128
|
+
{ ...meta, ruleName: policyResult.ruleName },
|
|
5129
|
+
hashAuditArgs
|
|
5130
|
+
);
|
|
4784
5131
|
return { approved: true, checkedBy: "local-policy" };
|
|
4785
5132
|
}
|
|
4786
5133
|
if (policyResult.decision === "block") {
|
|
@@ -4813,23 +5160,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4813
5160
|
{ ...meta, ruleName: policyResult.ruleName },
|
|
4814
5161
|
hashAuditArgs
|
|
4815
5162
|
);
|
|
4816
|
-
if (approvers.cloud && creds?.apiKey)
|
|
4817
|
-
auditLocalAllow(
|
|
4818
|
-
toolName,
|
|
4819
|
-
args,
|
|
4820
|
-
"smart-rule-block-override",
|
|
4821
|
-
creds,
|
|
4822
|
-
meta,
|
|
4823
|
-
void 0,
|
|
4824
|
-
false,
|
|
4825
|
-
{
|
|
4826
|
-
ruleName: policyResult.ruleName,
|
|
4827
|
-
ruleDescription: policyResult.ruleDescription,
|
|
4828
|
-
blockedByLabel: policyResult.blockedByLabel,
|
|
4829
|
-
matchedField: policyResult.matchedField,
|
|
4830
|
-
matchedWord: policyResult.matchedWord
|
|
4831
|
-
}
|
|
4832
|
-
);
|
|
4833
5163
|
const baseLabel = policyResult.blockedByLabel || "Smart Rule";
|
|
4834
5164
|
const OVERRIDE_PREFIX = "\u26A0\uFE0F Override block rule: ";
|
|
4835
5165
|
if (!baseLabel.startsWith(OVERRIDE_PREFIX)) {
|
|
@@ -4854,14 +5184,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4854
5184
|
{ ...meta, ruleName: policyResult.ruleName },
|
|
4855
5185
|
hashAuditArgs
|
|
4856
5186
|
);
|
|
4857
|
-
if (approvers.cloud && creds?.apiKey)
|
|
4858
|
-
auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
|
|
4859
|
-
ruleName: policyResult.ruleName,
|
|
4860
|
-
ruleDescription: policyResult.ruleDescription,
|
|
4861
|
-
blockedByLabel: policyResult.blockedByLabel,
|
|
4862
|
-
matchedField: policyResult.matchedField,
|
|
4863
|
-
matchedWord: policyResult.matchedWord
|
|
4864
|
-
});
|
|
4865
5187
|
return {
|
|
4866
5188
|
approved: false,
|
|
4867
5189
|
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
@@ -4890,8 +5212,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4890
5212
|
if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
|
|
4891
5213
|
const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
|
|
4892
5214
|
if (persistent === "allow") {
|
|
4893
|
-
if (approvers.cloud && creds?.apiKey)
|
|
4894
|
-
await auditLocalAllow(toolName, args, "persistent", creds, meta);
|
|
4895
5215
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
|
|
4896
5216
|
return { approved: true, checkedBy: "persistent" };
|
|
4897
5217
|
}
|
|
@@ -4924,8 +5244,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4924
5244
|
}
|
|
4925
5245
|
}
|
|
4926
5246
|
if (!taintWarning && getActiveTrustSession(toolName, args)) {
|
|
4927
|
-
if (approvers.cloud && creds?.apiKey)
|
|
4928
|
-
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
4929
5247
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
|
|
4930
5248
|
return { approved: true, checkedBy: "trust" };
|
|
4931
5249
|
}
|
|
@@ -5170,7 +5488,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
5170
5488
|
args,
|
|
5171
5489
|
finalResult.approved ? "allow" : "deny",
|
|
5172
5490
|
finalResult.checkedBy || finalResult.blockedBy || "unknown",
|
|
5173
|
-
|
|
5491
|
+
// cloudRequestId links this row to the BE-origin AuditLog row the
|
|
5492
|
+
// /intercept handshake created — the shipper hands it to the SaaS so
|
|
5493
|
+
// the BE enriches that row instead of inserting a duplicate. Matters
|
|
5494
|
+
// for EVERY racer outcome, not just cloud wins: a native-popup
|
|
5495
|
+
// decision on a cloud-pending request would otherwise count twice.
|
|
5496
|
+
cloudRequestId ? { ...meta, cloudRequestId } : meta,
|
|
5174
5497
|
hashAuditArgs
|
|
5175
5498
|
);
|
|
5176
5499
|
}
|